Застосунок для відео стрімінгу з низькою затримкою на базі FastApi

текст перекладу

Вступ

Почнемо з того, щоб трішки відсвяткувати це! Я вже давно планував написати технічний блог, і ось нарешті, ми тут! Надихнувшись легендами, такими як Себастьяно Рамірез, творцем FastAPI, я засучив рукави, розкрив арахіс і почав друкувати. І ось, фанфари! Так, арахіс також може мріяти про великі досягнення!

pic

Чому важлива низька затримка?

У світі потокового відео кожна мілісекунда має значення. Чи то ви дивитесь захоплюючий матч IPL на Hotstar, чи лайв-концерт, затримка може або покращити, або зіпсувати досвід. Уявіть світ, де кожен крик, здивування або удар м'яча відбувається в реальному часі — без незручних затримок, без дивних облич, що буферизуються. Ось це й є наша мета.

Щоб досягти цього, ми розглянемо, як побудувати додаток для потокового відео з низькою затримкою за допомогою FastAPI, сучасного веб-фреймворку, який є таким же ефективним, як і дружелюбним. І повірте, якщо FastAPI був би арахісом, це був би той самий ідеально підсмажений, за який всі борються.

pic

Використання: IP KVM рішення

У цьому блозі ми зосередимось на побудові додатку для потокового відео з низькою затримкою, призначеного для IP KVM (клавіатура, відео, миша) рішення. Це схоже на віддалений робочий стіл, але з додатковими вимогами до апаратного забезпечення. Цей приклад натхнений додатком, розробленим у організації, в якій я працюю, де низька затримка потоку була критично важливою вимогою.

В даному випадку ми зосередимося тільки на частині потоку відео — не на командах для клавіатури чи миші. Ідея полягає в тому, щоб показати, як можна досягти ефективного потокового відео за допомогою сучасних технологій, не забуваючи при цьому трохи розважитись.

Технології, які ми будемо використовувати:

  • FastAPI: для бекенд API.
  • FFmpeg: для кодування відео та потоку.
  • Vue.js: для інтерфейсу користувача.
  • WebCodecs API: для декодування відео потоків у браузері.

Декілька припущень

Цей блог припускає, що у вас є досвід у розробці веб-додатків, тому такі поняття, як HTTP запити, API і фреймворки для фронтенду, вам мають бути знайомі. Однак для розуміння кодування та декодування відео попереднє знання не є обов'язковим — ми покриємо основи по ходу справи.

Одне важливе зауваження: Це рішення підходить лише для використання в продакшн середовищі, якщо ваше застосування використовує безпечне з’єднання (HTTPS/WSS). WebCodecs API не доступний у незахищених контекстах, тому переконайтесь, що ваша конфігурація відповідає цій вимозі перед продовженням.

Початок: Простий FastAPI Endpoint

Розпочнемо з побудови дуже базового REST-ендпоінта за допомогою FastAPI. Це буде основа нашого бекенд-додатку. Ось код:

from fastapi import FastAPI  
import uvicorn  

APP = FastAPI()  

@APP.get("/")  
async def root():  
 """Повертає привітальне повідомлення.  

 Повертає:  
 dict: Словник, що містить привітальне повідомлення.  

 Примітки:  
 Це кореневий ендпоінт додатку.  
 """  
 return {"message": "Hello World"}  


if __name__ == "__main__":  
 config = uvicorn.Config(app=APP, host="0.0.0.0", port=5566, log_level="info")  
 server = uvicorn.Server(config)  
 server.run()APP = FastAPI()

Пояснення:

  • Ініціалізація FastAPI: Спочатку створюємо екземпляр FastAPI. Це основний об'єкт додатку, що обробляє всі запити та маршрути.
  • Кореневий ендпоінт: Декоратор @APP.get("/") визначає маршрут на кореневому шляху (/) додатку. Після доступу він повертає просте JSON повідомлення, {"message": "Hello World"}.
  • Запуск сервера: Блок if __name__ == "__main__": конфігурує і запускає сервер за допомогою Uvicorn. Тут ми вказуємо хост, порт та рівень логування.

Цей маленький, але потужний фрагмент коду задає основу для наших більш складних функцій.
текст перекладу

Вступ

Це як арахісове масло в смачному технічному сендвічі — просте, але необхідне!

Наївний підхід до потокового відео: JPEG кодування з Multipart HTTP відповіддю

Перед тим, як зануритися у складний світ потокового відео з низькою затримкою, давайте розглянемо простий, наївний метод передачі відео даних від сервера до клієнта. У цьому підході ми будемо використовувати JPEG кодування для кадрів відео і відправляти їх через multipart HTTP відповідь.

Що таке Multipart HTTP відповідь?

Multipart HTTP відповідь — це спосіб відправити кілька частин даних (наприклад, текст, бінарні файли, зображення) в одній HTTP відповіді. Цей метод часто використовують для потокової передачі даних, таких як зображення, кадри відео або великі файли, де дані розділені на частини (chunks) і відправляються по черзі. Кожна частина розділяється за допомогою обмежувача межі, і кожна частина може мати свої власні заголовки (наприклад, Content-Type).

Структура Multipart відповіді:

  1. Обмежувач межі: Унікальний рядок, що відокремлює частини. Починається з -, наприклад, -frame.
  2. Заголовки частини: Метадані частини, такі як Content-Type. Приклад: Content-Type: image/jpeg.
  3. Дані частини: Фактичний вміст частини (наприклад, бінарні дані зображення).
  4. Кінцева межа: Останній обмежувач, що завершує відповідь. Приклад: -frame--.

Приклад Multipart відповіді:

--frame\r\n # Відео кадр 1  
Content-Type: image/jpeg\r\n\r\n  
\r\n  
--frame\r\n # Відео кадр 2  
Content-Type: image/jpeg\r\n\r\n  
\r\n  
--frame-- # Кінець відповіді

Пояснення:

  • Обмежувач межі (-frame): Цей рядок розділяє різні частини відповіді.
  • Заголовок частини (Content-Type: image/jpeg): Визначає тип даних у частині. Тут це JPEG зображення.
  • Дані частини (): Містить фактичний вміст частини, яким є закодоване JPEG зображення.
  • Кінцева межа (-frame--): Позначає кінець multipart відповіді.

Потокова передача даних з FastAPI StreamingResponse

FastAPI надає клас StreamingResponse для потокової передачі даних клієнту. Це особливо корисно для відправки multipart відповідей, особливо для бінарних даних, таких як зображення або кадри відео.

Чому варто використовувати StreamingResponse?

  • Ефективність пам'яті: Потокова передача даних прямо з генератора без необхідності завантажувати все в пам'ять.
  • Трансмісія в реальному часі: Дає змогу серверу відправляти частини даних по мірі їх створення.
  • Користувацькі заголовки: Дозволяє встановлювати заголовки типу Content-Type для кожної частини відповіді.

Ми будемо використовувати StreamingResponse, щоб реалізувати цей наївний підхід до потокового відео. Цей метод не є найефективнішим для додатків з низькою затримкою, але він дає основне розуміння того, як можна передавати відео дані через HTTP.

Реалізація наївного ендпоінта для потокового відео

Тепер давайте втілимо це в код. Ось код для наївного ендпоінта для потокової передачі:

# Отримання шляху до поточного/кореневого файлу.  
ROOT_FILE: str = os.path.abspath(__file__)  

# Отримання поточної/кореневої директорії  
ROOT_DIRECTORY: str = os.path.dirname(ROOT_FILE)  

BACKEND_LOGGER: logging.Logger = logging.getLogger("BACKEND_LOGGER")  

KVM_NOT_CONNECTED_FILE_PATH: str = os.path.join(ROOT_DIRECTORY,  
 "assets",  
 "kvm_not_connected.jpg")  

with open(KVM_NOT_CONNECTED_FILE_PATH, "rb") as file_object:  
 KVM_NOT_CONNECTED_IMAGE: bytes = file_object.read()  

@APP.get("/kvm_stream/", response_model=None)  
async def open_kvm_stream() -> Union[StreamingResponse, FileResponse]:  
 """Відкрити ендпоінт для потокової передачі KVM, щоб захоплювати кадри з підключеного пристрою.  

 Цей ендпоінт використовує FastAPI StreamingResponse для безперервної  
 потокової передачі кадрів, захоплених з інтерфейсу OpenCV. Кадри  
 відправляються з заданим типом медіа "multipart/x-mixed-replace;  
 boundary=frame".  

 Повертає:  
 Union[StreamingResponse, FileResponse]: Потік кадрів з екрану KVM або  
 відповіді у вигляді JPEG зображення як fallback.

текст перекладу

videoindex: int = 0
backend
api: int = cv2.CAPMSMF
video
interface: cv2.VideoCapture = cv2.VideoCapture(videoindex, backendapi)
videointerface.set(cv2.CAPPROPFRAMEWIDTH, 1920)
videointerface.set(cv2.CAPPROPFRAMEHEIGHT, 1080)
videointerface.set(cv2.CAPPROP_FPS, 30)

try:
# Читання кадру з відео інтерфейсу kvm
readstatus, frame = videointerface.read()

if not readstatus:
return FileResponse(
KVM
NOTCONNECTEDFILEPATH,
media
type="image/jpeg")

except Exception:
return FileResponse(
KVMNOTCONNECTEDFILEPATH,
media_type="image/jpeg")

try:
# Повернення StreamingResponse, що безперервно передає кадри
return StreamingResponse(
readkvmvideoframes(videointerface),
media_type="multipart/x-mixed-replace;boundary=frame"
)

# Якщо виникає виняток (наприклад, помилка захоплення відео)
# Повернути відповідь у вигляді JPEG зображення
except Exception:
return FileResponse(KVMNOTCONNECTEDFILEPATH, media_type="image/jpeg")
```

Пояснення:

  1. Статичне зображення як fallback:
  • Ми визначаємо статичне JPEG зображення (kvm_not_connected.jpg), яке повертається, якщо потік відео KVM недоступний.

2. Відкриття відео потоку:

  • Ми використовуємо OpenCV (cv2.VideoCapture) для відкриття відео потоку з підключеного пристрою.

3. Обробка помилок:

  • Якщо відео потік не може бути відкритий або прочитаний, ми використовуємо fallback і повертаємо статичне зображення.

4. Потокова відповідь:

  • Якщо відео потік успішно відкрито, ендпоінт повертає об'єкт StreamingResponse.
  • Клас StreamingResponse в FastAPI призначений для потокової передачі даних клієнту по частинах. Він приймає генератор (функцію з оператором yield), що дозволяє серверу передавати дані поступово, не завантажуючи все в пам'ять одразу.
  • media_type встановлено в multipart/x-mixed-replace, що гарантує, що клієнт може інтерпретувати відповідь як потік JPEG зображень.

media_type="multipart/x-mixed-replace;boundary=frame" використовується для визначення формату multipart HTTP відповіді, де кілька частин даних (наприклад, зображення або відео кадри) відправляються по черзі в одній HTTP відповіді.

Розбір media_type="multipart/x-mixed-replace;boundary=frame"

  1. multipart/x-mixed-replace:
  • Multipart: Формат, що використовується для відправки кількох частин в одній HTTP відповіді.
  • x-mixed-replace: Цей підтип вказує, що відповідь буде безперервно передавати частини (як зображення або відео кадри), які можна замінювати новими. Це використовується для сценаріїв, де сервер продовжує надсилати оновлені дані з часом, таких як відео стрімінг.
  • Згідно з цією схемою, клієнт отримує серію даних (тут — відео кадри), які використовуються для відображення в браузері, замінюючи поточні дані (кадр) новими, коли сервер їх надсилає з часом.
  1. boundary=frame:
  • boundary — це обмежувач, що розділяє різні частини відповіді.
  • У цьому випадку обмежувач встановлено в frame, що означає, що кожна частина відповіді (наприклад, відео кадр або зображення) розділяється рядком --frame.
  • Обмежувач є важливим, оскільки він допомагає клієнту (наприклад, браузеру) розуміти, де починається і закінчується кожна частина.

Ця реалізація закладає основу для базового відео стрімінгового сервісу. Хоча цей метод не є найефективнішим для додатків з низькою затримкою, він надає чітке і функціональне розуміння того, як можна передавати відео дані через HTTP.

Реалізація генератора read_kvm_video_frames

def read_kvm_video_frames(video_interface: cv2.VideoCapture) -> ContentStream:  
 """Читає кадри з відео інтерфейсу kvm і повертає їх як JPEG байти.  

 Ця функція безперервно читає кадри з вказаного відео інтерфейсу kvm,  
 перетворює їх у JPEG формат і повертає байти.  
 Генератор працює безкінечно, поки не виникне виняток або клієнт не відключиться.

текст перекладу

videoindex: int = 0
backend
api: int = cv2.CAPMSMF
video
interface: cv2.VideoCapture = cv2.VideoCapture(videoindex, backendapi)
videointerface.set(cv2.CAPPROPFRAMEWIDTH, 1920)
videointerface.set(cv2.CAPPROPFRAMEHEIGHT, 1080)
videointerface.set(cv2.CAPPROP_FPS, 30)

try:
# Читання кадру з відео інтерфейсу kvm
readstatus, frame = videointerface.read()

if not readstatus:
return FileResponse(
KVM
NOTCONNECTEDFILEPATH,
media
type="image/jpeg")

except Exception:
return FileResponse(
KVMNOTCONNECTEDFILEPATH,
media_type="image/jpeg")

try:
# Повернення StreamingResponse, що безперервно передає кадри
return StreamingResponse(
readkvmvideoframes(videointerface),
media_type="multipart/x-mixed-replace;boundary=frame"
)

# Якщо виникає виняток (наприклад, помилка захоплення відео)
# Повернути відповідь у вигляді JPEG зображення
except Exception:
return FileResponse(KVMNOTCONNECTEDFILEPATH, media_type="image/jpeg")
```

Пояснення:

  1. Статичне зображення як fallback:
  • Ми визначаємо статичне JPEG зображення (kvm_not_connected.jpg), яке повертається, якщо потік відео KVM недоступний.

2. Відкриття відео потоку:

  • Ми використовуємо OpenCV (cv2.VideoCapture) для відкриття відео потоку з підключеного пристрою.

3. Обробка помилок:

  • Якщо відео потік не може бути відкритий або прочитаний, ми використовуємо fallback і повертаємо статичне зображення.

4. Потокова відповідь:

  • Якщо відео потік успішно відкрито, ендпоінт повертає об'єкт StreamingResponse.
  • Клас StreamingResponse в FastAPI призначений для потокової передачі даних клієнту по частинах. Він приймає генератор (функцію з оператором yield), що дозволяє серверу передавати дані поступово, не завантажуючи все в пам'ять одразу.
  • media_type встановлено в multipart/x-mixed-replace, що гарантує, що клієнт може інтерпретувати відповідь як потік JPEG зображень.

media_type="multipart/x-mixed-replace;boundary=frame" використовується для визначення формату multipart HTTP відповіді, де кілька частин даних (наприклад, зображення або відео кадри) відправляються по черзі в одній HTTP відповіді.

Розбір media_type="multipart/x-mixed-replace;boundary=frame"

  1. multipart/x-mixed-replace:
  • Multipart: Формат, що використовується для відправки кількох частин в одній HTTP відповіді.
  • x-mixed-replace: Цей підтип вказує, що відповідь буде безперервно передавати частини (як зображення або відео кадри), які можна замінювати новими. Це використовується для сценаріїв, де сервер продовжує надсилати оновлені дані з часом, таких як відео стрімінг.
  • Згідно з цією схемою, клієнт отримує серію даних (тут — відео кадри), які використовуються для відображення в браузері, замінюючи поточні дані (кадр) новими, коли сервер їх надсилає з часом.
  1. boundary=frame:
  • boundary — це обмежувач, що розділяє різні частини відповіді.
  • У цьому випадку обмежувач встановлено в frame, що означає, що кожна частина відповіді (наприклад, відео кадр або зображення) розділяється рядком --frame.
  • Обмежувач є важливим, оскільки він допомагає клієнту (наприклад, браузеру) розуміти, де починається і закінчується кожна частина.

Ця реалізація закладає основу для базового відео стрімінгового сервісу. Хоча цей метод не є найефективнішим для додатків з низькою затримкою, він надає чітке і функціональне розуміння того, як можна передавати відео дані через HTTP.

Реалізація генератора read_kvm_video_frames

def read_kvm_video_frames(video_interface: cv2.VideoCapture) -> ContentStream:  
 """Читає кадри з відео інтерфейсу kvm і повертає їх як JPEG байти.  

 Ця функція безперервно читає кадри з вказаного відео інтерфейсу kvm,  
 перетворює їх у JPEG формат і повертає байти.  
 Генератор працює безкінечно, поки не виникне виняток або клієнт не відключиться.

текст перекладу
## Перевірка, чи відкрито відео інтерфn'  
 b'Content-Type: image/jpeg\\r\\n\\r\\n' + KVM_NOT_CONNECTED_IMAGE + b'\\r\\n')  
 yield b'--frame--\\r\\n'  
 BACKEND_LOGGER.info("Інтерфейс KVM не відкривається, закриваємо потік.")  
 break
  • video_interface.isOpened() перевіряє, чи успішно відкрито об'єкт захоплення відео та підключено до пристрою.
  • Якщо з'єднання не вдалося (наприклад, пристрій недоступний), функція повертає JPEG зображення (KVM_NOT_CONNECTED_IMAGE), яке вказує, що пристрій не підключено.
  • Межа (--frame) включена для розділення частин multipart відповіді.
  • Відправка індикатора кінця відповіді --frame--\\r\\n клієнту, щоб позначити, що сервер більше не має даних для відправки.
  • Потім цикл переривається, припиняючи потік.

b. Читання кадру з пристрою KVM

read_status, frame = video_interface.read()
  • video_interface.read() намагається прочитати кадр з джерела відео KVM.
  • read_status — булева змінна, яка вказує, чи був успішно захоплений кадр. Якщо read_status — це False, це означає, що кадр не було захоплено.

c. Обробка помилок (невдале читання кадру)

if not read_status:  
 yield (b'--frame\\r\\n'  
 b'Content-Type: image/jpeg\\r\\n\\r\\n' + KVM_NOT_CONNECTED_IMAGE + b'\\r\\n')  
 yield b'--frame--\\r\\n'  
 BACKEND_LOGGER.info("Не вдалося прочитати новий кадр KVM, закриваємо потік.")  
 break
  • Якщо кадр не вдалося прочитати (наприклад, через проблему з камерою або з'єднанням), повертається fallback KVM_NOT_CONNECTED_IMAGE, як у попередній обробці помилок.
  • Потік потім зупиняється, виходячи з циклу.

d. Перетворення кадру в JPEG формат

_, encoded_image = cv2.imencode(".jpeg", frame)  
image_bytes = encoded_image.tobytes()
  • cv2.imencode(".jpeg", frame) перетворює сирий кадр у JPEG формат.
  • Отримане закодоване зображення перетворюється в байти за допомогою .tobytes(). Ці байти представляють дані JPEG зображення, яке буде відправлене клієнту.

e. Повернення кадру як частини multipart MIME

yield b"--frame\\r\\n" b"Content-Type: image/jpeg\\r\\n\\r\\n" + image_bytes + b"\\r\\n"
  • Кадр упаковується в multipart відповідь:
  • Межа --frame розділяє частини.
  • Content-Type: image/jpeg вказує, що частина містить JPEG зображення.
  • image_bytes — це фактичні дані зображення у форматі JPEG.
  • Цей кадр відправляється як частина multipart відповіді, і клієнт зможе відобразити його як зображення.

4. Обчислення FPS та скидання лічильника кадрів

frame_count += 1  
if time.time() - start_time > 1:  
 fps = frame_count / (time.time() - start_time)  
 BACKEND_LOGGER.info(f"KVM FPS: {int(fps)}.")  
 frame_count = 0  
 start_time = time.time()
  • Лічильник кадрів збільшується кожного разу, коли кадр успішно оброблено.
  • Якщо минула одна секунда (виміряна за допомогою time.time() - start_time), FPS обчислюється шляхом ділення кількості кадрів на час, що минув.
  • FPS записується за допомогою BACKEND_LOGGER.info.
  • Лічильник кадрів скидається на нуль, і start_time оновлюється до поточного часу для початку відліку наступної секунди.

5. Обробка помилок для генератора

except GeneratorExit:  
 BACKEND_LOGGER.error("Вихід з генератора для KVM.")  
except Exception:  
 BACKEND_LOGGER.error("Вихід з функції для KVM через загальний виняток.")
  • GeneratorExit ловиться, коли клієнт відключається або генератор явно закривається. Ця подія записується в лог.
  • Exception ловить інші помилки, що виникають під час виконання функції, щоб гарантовано закрити потік належним чином.

6.

текст перекладу

Останнє очищення

finally:  
 BACKEND_LOGGER.info("Закриваємо з'єднання.")  
 BACKEND_LOGGER.info(f"Звільняємо KVM для клієнта.")  
 video_interface.release()
  • Блок finally гарантує, що незалежно від того, що відбувається в блоках try-except, об'єкт захоплення відео буде звільнений, і всі пов'язані ресурси будуть очищені.
  • Потік закривається, і інтерфейс KVM звільняється назад до системи.

Підсумок

  • Безперервне потокове передавання: Функція передає кадри відео з пристрою KVM як JPEG зображення через HTTP у форматі multipart відповіді.
  • Обробка помилок: Якщо пристрій не підключено або кадр не можна прочитати, відправляється fallback зображення, і помилка записується в лог.
  • Показники продуктивності: Відстежує кількість кадрів на секунду (FPS) для моніторингу продуктивності потоку та записує FPS щосекунди.
  • Генератор: Ця функція є генератором, який нескінченно повертає кадри, поки не виникне помилка або клієнт не відключиться.

Загалом, цей код безперервно передає відео кадри клієнту ефективним, економним способом використовуючи генератор, при цьому надаючи реальний статистику FPS і обробку помилок.

Важливість JPEG кодування в відео стрімінгу

При роботі з відео кадрами, особливо в стрімінгових додатках, важливо розуміти різницю між сирими та стиснутими даними кадрів. Давайте розберемо це і підкреслимо важливість рядка _, encoded_image = cv2.imencode(".jpeg", frame) для досягнення ефективного відео стрімінгу.

Сирий кадр проти закодованого кадру

Сирий кадр з пристрою захоплення відео (наприклад, з камери) зазвичай представляється як багатовимірний масив, що містить значення пікселів для кожного кадру. Розмір цього кадру залежить від роздільної здатності та глибини кольору зображення.

Наприклад, 1080p кадр (1920x1080 пікселів) з 24-бітним кольором (8 біт для кожного каналу червоний, зелений, синій) буде мати розмір приблизно:

Розмір сирого кадру = 1920 * 1080 * 3 байти = 6.22 МБ

Цей сирий кадр великий і не стислий, що означає, що для його передачі через мережу буде потрібно значну кількість пропускної здатності.

Що таке кодування?

Кодування — це процес стиснення або перетворення сирого кадру в менший, більш ефективний формат для зменшення розміру файлу і вимог до мережі. Найбільш поширеними форматами кодування для відео стрімінгу є:

  • JPEG (Joint Photographic Experts Group): Метод стиснення з втратами, який значно зменшує розмір файлу, зберігаючи при цьому розумну якість.
  • PNG (Portable Network Graphics): Формат стиснення без втрат, що зберігає якість зображення без втрат. Хоча він чудово підходить для зображень з прозорістю або дрібними деталями, він дає значно більші розміри файлів порівняно з JPEG, тому він менш підходить для відео стрімінгу, де важлива ефективність пропускної здатності.
  • WEBP: Сучасний формат зображень, розроблений Google, що пропонує як стиснення без втрат, так і стиснення з втратами. Він забезпечує менші розміри файлів, ніж JPEG, при подібній якості, що робить його ефективним вибором для веб-зображень. Однак його підтримка на всіх платформах може бути не такою широко поширеною, як у JPEG, хоча він набирає популярності для стиснення зображень у веб-додатках.

Для цього блогу ми зосереджуємося на JPEG кодуванні, яке широко використовується для стиснення зображень і відносно просте для впровадження.

Як працює JPEG кодування

JPEG працює, використовуючи методи стиснення з втратами для зменшення розміру файлів зображень. Він використовує такі методи, як:

  1. Перетворення кольорового простору: Значення RGB перетворюються на YCbCr, розділяючи яскравість (Y) і кольорову інформацію (Cb і Cr).
  2. DCT (Дискретне косинусне перетворення): Зображення ділиться на маленькі блоки (8x8 пікселів), і кожен блок перетворюється на частотні компоненти.
  3. Квантизація: Менш важливі частотні компоненти відкидаються, що значно зменшує розмір даних.
  4. Кодування ентропії: Залишкові дані кодуються за допомогою таких методів, як кодування Хаффмана, щоб додатково зменшити розмір.

Переваги JPEG кодування

1.
текст перекладу

Останнє очищення

finally:  
 BACKEND_LOGGER.info("Закриваємо з'єднання.")  
 BACKEND_LOGGER.info(f"Звільняємо KVM для клієнта.")  
 video_interface.release()
  • Блок finally гарантує, що незалежно від того, що відбувається в блоках try-except, об'єкт захоплення відео буде звільнений, і всі пов'язані ресурси будуть очищені.
  • Потік закривається, і інтерфейс KVM звільняється назад до системи.

Підсумок

  • Безперервне потокове передавання: Функція передає кадри відео з пристрою KVM як JPEG зображення через HTTP у форматі multipart відповіді.
  • Обробка помилок: Якщо пристрій не підключено або кадр не можна прочитати, відправляється fallback зображення, і помилка записується в лог.
  • Показники продуктивності: Відстежує кількість кадрів на секунду (FPS) для моніторингу продуктивності потоку та записує FPS щосекунди.
  • Генератор: Ця функція є генератором, який нескінченно повертає кадри, поки не виникне помилка або клієнт не відключиться.

Загалом, цей код безперервно передає відео кадри клієнту ефективним, економним способом використовуючи генератор, при цьому надаючи реальний статистику FPS і обробку помилок.

Важливість JPEG кодування в відео стрімінгу

При роботі з відео кадрами, особливо в стрімінгових додатках, важливо розуміти різницю між сирими та стиснутими даними кадрів. Давайте розберемо це і підкреслимо важливість рядка _, encoded_image = cv2.imencode(".jpeg", frame) для досягнення ефективного відео стрімінгу.

Сирий кадр проти закодованого кадру

Сирий кадр з пристрою захоплення відео (наприклад, з камери) зазвичай представляється як багатовимірний масив, що містить значення пікселів для кожного кадру. Розмір цього кадру залежить від роздільної здатності та глибини кольору зображення.

Наприклад, 1080p кадр (1920x1080 пікселів) з 24-бітним кольором (8 біт для кожного каналу червоний, зелений, синій) буде мати розмір приблизно:

Розмір сирого кадру = 1920 * 1080 * 3 байти = 6.22 МБ

Цей сирий кадр великий і не стислий, що означає, що для його передачі через мережу буде потрібно значну кількість пропускної здатності.

Що таке кодування?

Кодування — це процес стиснення або перетворення сирого кадру в менший, більш ефективний формат для зменшення розміру файлу і вимог до мережі. Найбільш поширеними форматами кодування для відео стрімінгу є:

  • JPEG (Joint Photographic Experts Group): Метод стиснення з втратами, який значно зменшує розмір файлу, зберігаючи при цьому розумну якість.
  • PNG (Portable Network Graphics): Формат стиснення без втрат, що зберігає якість зображення без втрат. Хоча він чудово підходить для зображень з прозорістю або дрібними деталями, він дає значно більші розміри файлів порівняно з JPEG, тому він менш підходить для відео стрімінгу, де важлива ефективність пропускної здатності.
  • WEBP: Сучасний формат зображень, розроблений Google, що пропонує як стиснення без втрат, так і стиснення з втратами. Він забезпечує менші розміри файлів, ніж JPEG, при подібній якості, що робить його ефективним вибором для веб-зображень. Однак його підтримка на всіх платформах може бути не такою широко поширеною, як у JPEG, хоча він набирає популярності для стиснення зображень у веб-додатках.

Для цього блогу ми зосереджуємося на JPEG кодуванні, яке широко використовується для стиснення зображень і відносно просте для впровадження.

Як працює JPEG кодування

JPEG працює, використовуючи методи стиснення з втратами для зменшення розміру файлів зображень. Він використовує такі методи, як:

  1. Перетворення кольорового простору: Значення RGB перетворюються на YCbCr, розділяючи яскравість (Y) і кольорову інформацію (Cb і Cr).
  2. DCT (Дискретне косинусне перетворення): Зображення ділиться на маленькі блоки (8x8 пікселів), і кожен блок перетворюється на частотні компоненти.
  3. Квантизація: Менш важливі частотні компоненти відкидаються, що значно зменшує розмір даних.
  4. Кодування ентропії: Залишкові дані кодуються за допомогою таких методів, як кодування Хаффмана, щоб додатково зменшити розмір.

Переваги JPEG кодування

  1. Зменшення розміру файлу: JPEG кодування значно зменшує розмір зображення, що полегшує його передачу через мережі з обмеженою пропускною здатністю.
  2. Зменшення вимог до мережі: Менші закодовані кадри зменшують загальні вимоги до пропускної здатності мережі для стрімінгу, що є важливим для застосувань з низькою затримкою.
  3. Баланс між якістю та стисненням: JPEG дозволяє регулювати рівні стиснення. Збільшуючи стиснення, можна зменшити розмір файлу за рахунок якості і навпаки.

Приклад: Сирий кадр проти JPEG закодованого кадру

Давайте порівняємо розмір сирого кадру та його JPEG-кодованого аналога:

  • Розмір сирого кадру (1080p, 24-бітний колір): 6.22 МБ
  • Розмір JPEG закодованого кадру (після стиснення): 241 КБ

Використовуючи JPEG кодування, ми зменшуємо розмір кожного кадру, що в свою чергу зменшує пропускну здатність, необхідну для стрімінгу.

Вимоги до мережі

Пропускна здатність мережі, необхідна для стрімінгу відео, прямо пропорційна розміру кадрів, що передаються. Давайте розрахуємо пропускну здатність, необхідну для передачі сирих та JPEG-кодованих кадрів:

  • Вимоги до пропускної здатності сирих кадрів:
Розмір сирого кадру (6.22 МБ) * Частота кадрів (30 FPS) = 186.6 МБ на секунду
  • Вимоги до пропускної здатності JPEG-кодованих кадрів:
Розмір JPEG кадру (241 КБ) * Частота кадрів (30 FPS) = 7.23 МБ на секунду

Стиснення кожного кадру за допомогою JPEG кодування значно зменшує вимоги до пропускної здатності для стрімінгу. Це робить можливим стрімінг відео через мережі з нижчою швидкістю та забезпечує плавну роботу для додатків реального часу.

Підсумовуючи, рядок _, encoded_image = cv2.imencode(".jpeg", frame) відіграє критичну роль у перетворенні великого, сирого відео кадру в стиснений, зручний розмір, що мінімізує навантаження на мережу і покращує загальну ефективність відео стрімінгового додатку.

Повна реалізація

Ось повний код:

# Вбудовані імпорти  
import logging.config  
import os  
import time  
from typing import Union  

# Зовнішні імпорти  
import cv2  
from fastapi import FastAPI  
from fastapi.responses import FileResponse, StreamingResponse  
from starlette.responses import ContentStream  
import uvicorn  

# Отримуємо місце розташування поточного/кореневого файлу.  
ROOT_FILE: str = os.path.abspath(__file__)  

# Отримуємо поточний/кореневий каталог  
ROOT_DIRECTORY: str = os.path.dirname(ROOT_FILE)  

BACKEND_LOGGER: logging.Logger = logging.getLogger("BACKEND_LOGGER")  
# Встановлюємо рівень логування  
BACKEND_LOGGER.setLevel(logging.DEBUG)  

# Створюємо обробник для консолі  
CONSOLE_HANDLER = logging.StreamHandler()  
# Встановлюємо рівень логування для обробника  
CONSOLE_HANDLER.setLevel(logging.DEBUG)  

# Створюємо форматувальник та додаємо його до обробника  
FORMATTER = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')  
CONSOLE_HANDLER.setFormatter(FORMATTER)  

# Додаємо обробник до логера  
BACKEND_LOGGER.addHandler(CONSOLE_HANDLER)  

APP: FastAPI = FastAPI()  


@APP.get("/")  
async def root() -> dict:  
 """Повертає вітальне повідомлення.  

 Повертає:  
 dict: Словник, що містить вітальне повідомлення.  

 Примітки:  
 Це кореневий endpoint додатка.  
 """  
 return {"message": "Hello World"}  


KVM_NOT_CONNECTED_FILE_PATH: str = os.path.join(ROOT_DIRECTORY,  
 "assets",  
 "kvm_not_connected.jpg")  

with open(KVM_NOT_CONNECTED_FILE_PATH, "rb") as file_object:  
 KVM_NOT_CONNECTED_IMAGE: bytes = file_object.read()  


def read_kvm_video_frames(video_interface: cv2.VideoCapture) -> ContentStream:  
 """Читає кадри з відео інтерфейсу KVM і повертає їх як байти JPEG.  

 Ця функція безперервно читає кадри з вказаного відео інтерфейсу KVM,  
 перетворює їх у формат JPEG і повертає їх як байти.  
 Генератор працює нескінченно, поки не виникне помилка або клієнт  
 не відключиться.
текст перекладу
Аргументи:  
 video_interface (cv2.VideoCapture): OpenCV відео інтерфейс до підключеного пристрою.  

 Повертає:  
 bytes: Дані кадру у форматі multipart MIME з типом контенту JPEG.  
 """  
 frame_count = 0  

 # Час початку для обчислення FPS  
 start_time = time.time()  

 try:  
 # Цикл до тих пір, поки існує з'єднання клієнта  
 while True:  
 # Перевірка, чи успішно відкрито захоплення відео  
 if not video_interface.isOpened():  
 yield (b'--frame\r\n'  
 b'Content-Type: image/jpeg\r\n\r\n' +  
 KVM_NOT_CONNECTED_IMAGE + b'\r\n')  
 yield b'--frame--\r\n'  
 BACKEND_LOGGER.info("Інтерфейс KVM не відкривається, закриваючи потік.")  
 break  

 # Читання кадру з об'єкта відео KVM  
 read_status, frame = video_interface.read()  

 if not read_status:  
 yield (b'--frame\r\n'  
 b'Content-Type: image/jpeg\r\n\r\n' +  
 KVM_NOT_CONNECTED_IMAGE + b'\r\n')  
 yield b'--frame--\r\n'  
 BACKEND_LOGGER.info("Не вдалося прочитати новий кадр KVM, закриваючи потік.")  
 break  
 else:  
 # Перетворення кадру в формат JPEG  
 _, encoded_image = cv2.imencode(".jpeg", frame)  

 image_bytes = encoded_image.tobytes()  

 # Повернення даних кадру у форматі multipart MIME з типом контенту JPEG  
 yield b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + image_bytes + b"\r\n"  

 frame_count += 1  

 # Перевірка, чи минула одна секунда  
 if time.time() - start_time > 1:  
 # Обчислення поточного FPS  
 fps = frame_count / (time.time() - start_time)  

 BACKEND_LOGGER.info(f"KVM FPS: {int(fps)}.")  

 # Скидання кількості кадрів в нуль  
 frame_count = 0  

 # Скидання часу початку на поточний час  
 start_time = time.time()  
 except GeneratorExit:  
 BACKEND_LOGGER.error("Вихід з генератора для KVM.")  
 except Exception:  
 BACKEND_LOGGER.error("Вихід з функції для KVM через загальну помилку.")  
 finally:  
 BACKEND_LOGGER.info("Закриваємо підключення SSE.")  
 BACKEND_LOGGER.info(f"Звільняємо KVM для клієнта.")  

 video_interface.release()  


@APP.get("/kvm-stream/", response_model=None)  
async def open_kvm_stream() -> Union[StreamingResponse, FileResponse]:  
 """Відкрити кінцеву точку потокового передавання KVM для захоплення кадрів з підключеного пристрою.  

 Ця кінцева точка використовує StreamingResponse FastAPI для безперервного  
 потокового передавання кадрів, захоплених з інтерфейсу OpenCV. Кадри  
 надсилаються з вказаним типом медіа "multipart/x-mixed-replace;  
 boundary=frame".  

 Повертає:  
 Union[StreamingResponse, FileResponse]: або StreamingResponse, яка  
 безперервно передає кадри з дисплея KVM, або FileResponse  
 з JPEG зображенням як запасним варіантом.
текст перекладу
"""  
 video_index: int = 0  
 backend_api: int = cv2.CAP_MSMF  
 video_interface: cv2.VideoCapture = cv2.VideoCapture(video_index, backend_api)  
 video_interface.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)  
 video_interface.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)  
 video_interface.set(cv2.CAP_PROP_FPS, 30)  

 try:  
 # Читання кадру з об'єкта відео KVM  
 read_status, frame = video_interface.read()  

 if not read_status:  
 return FileResponse(  
 KVM_NOT_CONNECTED_FILE_PATH,  
 media_type="image/jpeg")  

 except Exception:  
 return FileResponse(  
 KVM_NOT_CONNECTED_FILE_PATH,  
 media_type="image/jpeg")  

 try:  
 # Повернення StreamingResponse, що безперервно передає кадри  
 return StreamingResponse(  
 read_kvm_video_frames(video_interface),  
 media_type="multipart/x-mixed-replace;boundary=frame"  
 )  

 # Якщо виникає виняток (наприклад, помилка захоплення відео)  
 # Повертаємо jpeg зображення як відповідь  
 except Exception:  
 return FileResponse(KVM_NOT_CONNECTED_FILE_PATH, media_type="image/jpeg")  


if __name__ == "__main__":  
 config = uvicorn.Config(app=APP, host="0.0.0.0", port=5566, log_level="info")  

 server = uvicorn.Server(config)  
 server.run()

Налаштування фронтенду з Vue.js

Ми будемо використовувати Vue.js — легку та зручну для початківців JavaScript-рамку для створення нашого фронтенду.

Початкові кроки для налаштування Vue.js

  1. Створіть новий проект Vue:

Запустіть наступну команду, щоб створити новий додаток Vue.js:

npm create vue@latest

Це запитає вас вибрати ім'я проекту та інші налаштування. Ви можете залишити стандартні варіанти для спрощення.

  1. Перейдіть в директорію проекту:

Після створення проекту перейдіть в його папку:

cd
  1. Встановіть залежності:

Встановіть необхідні пакети та залежності для Vue проекту:

npm install

Додавання Tailwind CSS у ваш Vue.js проект

Для стилізації нашого фронтенду зручно і гнучко ми інтегруємо Tailwind CSS в наш Vue.js додаток. Tailwind — це CSS фреймворк "перш за все утиліт", який дозволяє створювати сучасні адаптивні дизайни без виходу з HTML.

Кроки для додавання Tailwind CSS

  1. Встановіть Tailwind та супутні залежності:

Запустіть наступну команду для установки Tailwind CSS разом з postcss та autoprefixer:

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
  1. Ініціалізація конфігурації Tailwind CSS:

Ініціалізуйте файл конфігурації Tailwind за допомогою:

npx tailwindcss init -p

Це створить файл tailwind.config.js у корені вашого проекту, який ви можете налаштувати за потребою.

  1. Додайте Tailwind в ваш CSS:

Створіть новий CSS файл для підключення базових стилів, компонентів та утиліт Tailwind.

  • Створіть файл: ./src/assets/main.css.
  • Додайте наступний вміст:
/* ./src/assets/main.css */  
@tailwind base;  
@tailwind components;  
@tailwind utilities;
  1. Імпортуйте Tailwind CSS у ваш Vue проект:

У головному JavaScript файлі (зазвичай main.js) переконайтеся, що ви імпортуєте файл Tailwind CSS, який ви щойно створили (якщо цього ще не зробили):

import './assets/main.css';
  1. Запустіть сервер розробки:

Щоб перевірити, чи все налаштовано правильно, запустіть локальний сервер розробки:

npm run dev

Ви побачите URL (наприклад, http://localhost:5173), де ваш додаток працює. Відкрийте його в браузері, щоб переглянути стартовий шаблон.

Огляд структури папок

У цьому розділі ми розглянемо базову структуру папок Vue.js проекту, зосередившись на основних файлах і папках, що мають значення на цьому етапі процесу розробки.

1. Папка src

Папка src містить всю основну логіку додатка та активи. Це місце, де ви проведете більшу частину часу, працюючи над проектом Vue.js.

2. main.js

Це точка входу для додатка Vue.js.
текст перекладу
Давайте розглянемо вміст і зрозуміємо його призначення:

import './assets/main.css';  
import { createApp } from 'vue';  

import App from './App.vue';  
import router from './router';  

const app = createApp(App);  

app.use(router);  

app.mount('#app');
  • Імпорт CSS:

Файл main.css (в середині папки assets) імпортується тут для застосування глобальних стилів за допомогою Tailwind CSS.

  • Створення додатка Vue:

createApp(App) ініціалізує головний додаток Vue, використовуючи кореневий компонент App.vue.

  • Маршрутизація:

router (налаштований у папці router) додається до додатка для керування навігацією між сторінками.

  • Монтування додатка:

Нарешті, додаток монтується до DOM елемента з ID #app, який зазвичай знаходиться у файлі index.html.

  1. App.vue

Це кореневий компонент додатка, який виступає як батьківський для всіх інших компонентів.






  • RouterView:

Це вбудований компонент Vue, який відображає компонент, що відповідає поточному маршруту. Він виступає як місце для динамічно завантажених представлень.

  • Обмежений стиль:

``
текст перекладу
* @param {none} None
* @return {none} None
*/
function handleImageError() {

// Оновлення стану, коли зображення потоку KVM не вдалося завантажити
isStreamLoaded.value = false;
}

/*
<---------- Підключення до серверу ---------->
*/
// Дані

// Реактивні дані

const isLoading = ref(true);

// Хуки життєвого циклу
onBeforeMount(async ()=>{
isLoading.value = false;
})


Цей розділ надає детальне пояснення його структури, функціональності та основних аспектів.

## Розбір коду

## 1. Розділ скрипта
текст перекладу
Scoped styles (Обмежені стилі) забезпечують застосування CSS лише до цього компонента, не впливаючи на інші.

## Як це працює

1. **Життєвий цикл:**

- Перед монтуванням компонента виконується логіка налаштування за допомогою `onBeforeMount`.

**2. Обробка потоку:**

- Відеопотік отримується з бекенду та відображається за допомогою першого тега `<img>`.
- Якщо виникає помилка (наприклад, бекенд недоступний), відображається зображення SVG як резервне.

**3. Реактивні оновлення:**

- Система реактивності Vue забезпечує безперебійну оновлення UI на основі успішності або невдачі потоку.

Цей компонент надає чистий і ефективний спосіб відображення динамічних відеопотоків з одночасним обробленням помилок.

## Як браузер обробляє відео-стрімінг

Одним з найцікавіших аспектів цієї реалізації є те, як сам браузер виконує важку роботу, необхідну для безперебійного відео-стрімінгу. Цей розділ пояснює взаємодію між фронтендом і бекендом, підкреслюючи ефективність використання можливостей браузера.

## Роль бекенду

Бекенд надсилає відеодані у вигляді послідовності окремих кадрів, кожен з яких закодовано як зображення JPEG. Це досягається за допомогою наступного коду:

StreamingResponse(
readkvmvideoframes(videointerface),
media_type="multipart/x-mixed-replace;boundary=frame"
)
```

  • StreamingResponse: Подає дані в реальному часі з бекенду на клієнта.
  • media_type="multipart/x-mixed-replace;boundary=frame": Вказує, що відповідь містить кілька частин, кожна з яких представляє кадр відео, а також визначає frame як межу між частинами, дозволяючи браузеру розпізнати, де закінчується один кадр і починається наступний.

Бекенд фактично перетворює відео в реальному часі на послідовність зображень JPEG та стрімить їх у безперервному multipart-відповіді.

Відповідальність браузера

Коли браузер отримує цю відповідь, він інтерпретує заголовок Content-Type (multipart/x-mixed-replace) і виконує кілька ключових завдань:

  1. Парсинг та декодування кадрів:
  • Браузер розпізнає директиву boundary=frame, яка позначає початок і кінець кожного кадру в потоці.
  • Він парсить кожну частину відповіді, ідентифікує дані, закодовані у форматі JPEG, та декодує їх для відображення.

2. Оновлення тега <img>:

  • Тег <img> на фронтенді прив’язаний до URL потоку з бекенду через атрибут src:
<img :src="streamingUrl" alt="Video Stream" />
  • Як тільки браузер декодує кожен кадр, він замінює поточне зображення новим кадром, що дає ілюзію безперервного відео, хоча кожен кадр обробляється як окреме статичне зображення.

3. Ефективне декодування:

  • Декодування JPEG обробляється безпосередньо браузером, який оптимізований для таких операцій. Це знімає обчислювальне навантаження з додатку, забезпечуючи плавний стрімінг навіть на пристроях з обмеженими ресурсами.

Відео як послідовність зображень

Важливо розуміти, що відео — це по суті послідовність статичних зображень (кадрів), що відображаються швидко один за одним. Наприклад:

  • Типове відео може відтворюватися на 30 кадрів на секунду (fps).
  • Бекенд генерує та стрімить 30 кадрів JPEG на секунду.
  • Браузер замінює відображуване зображення 30 разів на секунду, створюючи ілюзію безперервного руху.

Переваги JPEG мультчастинного стрімінгу

  1. Ефективність, вбудована в браузер: Вбудована підтримка браузера для обробки multipart-відповідей, парсингу меж, декодування JPEG зображень та оновлення тегів <img> усуває необхідність в розробці власної клієнтської логіки.
  2. Безперебійні оновлення: Браузер автоматично видаляє старий кадр і відображає новий, коли він прибуває, забезпечуючи плавне та безперервне відтворення.
    текст перекладу
    Крос-платформна сумісність: Цей підхід працює з сучасними браузерами "з коробки", без необхідності додаткових бібліотек чи плагінів.
  3. Зменшена складність: Перекладаючи завдання, як-от декодування та відображення, на браузер, додаток може зосередитись на ефективному генеруванні та стрімінгу відеоданих.

Підсумок

Цей дизайн використовує переваги як бекенду, так і браузера:

  • Бекенд ефективно стрімить легкі, закодовані кадри.
  • Браузер виконує обчислювально інтенсивні завдання парсингу, декодування та рендерингу відео.

Разом вони створюють дуже ефективну та підтримувану архітектуру для доставки потокових відео.

Обмеження цього підходу

Хоча підхід використання браузера для відео-стрімінгу є ефективним і простим у впровадженні, він має певні обмеження, в основному через обмежений контроль над основними процесами. Ось деякі ключові обмеження:

1. Обмежений контроль над HTTP заголовками

  • Проблеми з автентифікацією:
  • У виробничому середовищі часто використовують механізми автентифікації, як-от JWT токени для захисту відеопотоків.
  • Однак, оскільки браузер обробляє запит на відео-потік (атрибут src в <img>), розробники не можуть вручну змінювати заголовки цих запитів, щоб додавати токени автентифікації.
  • Є способи обійти: Хоча існують способи додати автентифікацію, вони не завжди прості чи ідеальні.

2. Відсутність розрахунку FPS на клієнтській стороні

  • Браузер обробляє декодування та відображення кадрів безпосередньо, не надаючи хук або події, щоб розробники могли перехоплювати моменти, коли новий кадр відображається.
  • Це обмеження ускладнює розрахунок таких метрик, як кадри на секунду (FPS) на клієнтській стороні, оскільки немає прямого доступу до часу оновлення кадрів.

3. Знижена гнучкість

Оскільки браузер абстрагує більшість роботи (декодування, парсинг і рендеринг), розробники втрачають контроль над кастомною обробкою або розширеними сценаріями використання для відео кадрів.

4. Компроміс між легкістю та контролем

Хоча цей підхід є надзвичайно легким для впровадження і вимагає мінімуму коду на фронтенді, він жертвує гнучкістю та детальним контролем, що може бути необхідним для більш складних чи захищених додатків.

Висновок

Цей метод ідеально підходить для швидких реалізацій або ситуацій, де простота важливіша за контроль. Однак для випадків, які вимагають захищеної автентифікації, клієнтської обробки або розширених функцій, розробники можуть розглянути альтернативні методи або більш складні обхідні шляхи.

Результати тестів продуктивності та обмеження мережі

Для оцінки продуктивності нашого поточного підходу я провів серію тестів, щоб перевірити його життєздатність у реальних умовах. Ось що ми знайшли:

1. FPS бекенду

Бекенд успішно підтримував FPS 25–30, що досить добре для стрімінгу відео 720p.

2. Розмір кадру

  • Розмір кожного кадру реєструється за допомогою наступного рядка:
BACKEND_LOGGER.info(f"Size of data: {int(len(image_bytes) / 1024)} Kb.")
  • Розмір кожного кадру становить близько 240 КБ, що значно для одного зображення.

3. Вимоги до пропускної здатності

  • Для досягнення плавного стрімінгу при 30 FPS нам потрібно: 240 КБ × 30 = 7200 КБ на секунду, або приблизно 7.2 МБ/с (мегабайти на секунду).
  • Однак середня швидкість інтернету в Індії становить лише близько 5–6 МБ/с, а при використанні VPN або корпоративних мереж швидкість може зменшитись ще більше.

Перспектива з горіхом

Давайте скажемо так: стрімити таким способом — це як намагатися з’їсти 30 сендвічів з арахісовим маслом за хвилину. Звісно, ви можете почати сильно, але в кінці або будете задихатися, або потонете в арахісовому маслі. 🥜 Уявіть, що ваш інтернет захлинається відео-даними, як це.
текст перекладу
Не дуже весело, чи не так?

pic

Необхідність оптимізації

  • Поточний підхід простий і працює, але вимоги до пропускної здатності занадто високі для стабільної роботи, особливо в умовах обмежених мереж.
  • Далі ми розглянемо оптимізований метод, що використовує відео кодування, яке значно знижує використання пропускної здатності при збереженні якості.

Таємниця пікселів: випадок з рухомою мишею

Уявіть собі: ви спостерігаєте за відображенням екрана віддаленого пристрою протягом 2 секунд при 30 кадрах в секунду. Це 60 кадрів, кожен — складний витвір пікселів, що зливаються разом для створення безшовного руху. Але почекайте — щось дивне привертає вашу увагу.

pic

Сцена

Перший кадр показує покажчик миші в точці А, спокійно стоїть. До кінця останнього кадру він рухається до точки B, зміщаючись лише на невелику відстань. І ось цікаве: у всіх цих 60 кадрах нічого більше не змінилося. Екран залишався тим самим — заморожений фон з лише невеликими змінами пікселів поруч з покажчиком миші.

Питання

А чому ж надсилати всі пікселі для всіх кадрів, коли більшість із них просто тихо виконують свою роботу, залишаючись незмінними? Чи заслуговує кожен піксель на рівний час ефіру? Хіба не достатньо зосередитись лише на тих, що вирішили створити якийсь рух?

Коли ви обмірковуєте цю неефективність, арахіс безсоромно котиться по вашому столу, насміхаючись над вашими проблемами з пропускною здатністю. "Навіщо відправляти цілу банку," здається, він говорить, "коли достатньо лише ложки?"

pic

Підказка

Невже не було б геніально сказати: "Гей, між цими кадрами змінилося лише кілька речей — навколо покажчика миші, наприклад. Давайте відправимо лише ці зміни і заощадимо всім клопіт з відправкою зайвих пікселів!"

Розкриття

І ось, це стає очевидно — ідея настільки елегантна, настільки ефективна, що здається, ніби рішення було прямо перед очима все цей час. Метод, що дозволяє зафіксувати лише зміни між кадрами, позбавляючи мережу від необхідності щоразу відправляти все знову.

Вітаємо, відео кодування.

Що таке відео кодування?

Відео кодування — це процес стиснення сирих відеоданих для зменшення їх розміру при збереженні максимальної якості. Воно працює шляхом виявлення і усунення надмірностей між кадрами, ефективно кодуючи зміни (дельти) між ними. Замість того, щоб відправляти всі пікселі кожного кадру, відео кодування зосереджується лише на нових або змінених елементах.

Цей підхід значно знижує вимоги до пропускної здатності для стрімінгу, дозволяючи доставляти плавне, високо якісне відео навіть через обмежені мережі.

Як працює відео кодування?

  1. Ключові кадри (I-Frames): Це повні зображення, подібні до JPEG. Вони служать опорними точками для послідовності.
  2. Прогнозовані кадри (P-Frames): Вони зберігають лише зміни від попереднього кадру.
  3. Двонаправлені кадри (B-Frames): Вони враховують як попередні, так і наступні кадри для більш ефективного кодування змін.

Поєднуючи ці типи кадрів, відео кодування мінімізує надмірну передачу даних, забезпечуючи при цьому безперебійне відтворення.

Типи стандартів відео кодування

Ось кілька поширених стандартів відео кодування:

  • H.264 (AVC): Найпоширеніший стандарт, що забезпечує хорошу баланс між якістю та ефективністю стиснення.
  • H.265 (HEVC): Наслідувач H.264, з кращим стисненням для тієї ж якості, особливо для відео високої роздільної здатності, як 4K.
  • VP9 та AV1: Відкриті альтернативи, оптимізовані для веб-стрімінгу.

Чому важливе відео кодування

Без відео кодування стрімінг навіть короткого відео вимагав би величезних обсягів даних.
текст перекладу
Ефективно упаковуючи та передаючи лише необхідні зміни, відео кодування забезпечує більш плавне відтворення, знижене буферизацію та зменшення вимог до мережі, що робить його основою сучасного відео стрімінгу.

Кодування H.264: глибоке занурення

H.264 — один з найпоширеніших стандартів стиснення відео, розроблений для досягнення балансу між якістю відео та ефективністю стиснення. Це досягається шляхом розбиття відео на кадри та їх кодування таким чином, що зменшується надмірність. У цьому розділі ми розглянемо деякі ключові концепції та формати, що використовуються в кодуванні H.264, зокрема NAL дані та формат Annex B.

1. Одиниці NAL: будівельні блоки H.264

Що таке одиниці NAL?

Одиниці абстракції мережі (NAL) є основними одиницями закодованих відео даних H.264. Вони інкапсулюють фактичні відео дані (як-от кадри) та додаткові метадані, необхідні для декодування. Кожна одиниця NAL містить:

  • Заголовок: вказує на тип даних в одиниці NAL.
  • Payload: закодовані відео дані або додаткову інформацію.

Типи одиниць NAL

Деякі поширені типи одиниць NAL включають:

  • VCL (Video Coding Layer) Units: несуть фактичні стиснуті відео дані. Приклад:
    • I-Frames (Ключові кадри): Повні зображення, які використовуються як орієнтири.
    • P-Frames: Кодують різницю між попереднім і поточним кадром.
    • B-Frames: Кодують різницю, використовуючи як минулі, так і майбутні кадри.
  • Non-VCL Units: включають метадані, такі як заголовки послідовностей, параметри та інше.

2. Формати H.264: Annex B vs. Length-Prefixed

Дані H.264 можуть зберігатися або передаватися у двох основних форматах:

Формат Annex B

  • Annex B — це формат байтового потоку, призначений для низької затримки при стрімінгу.
  • Кожна одиниця NAL передується з стартовим кодом:
    • 0x000001 (3 байти) або 0x00000001 (4 байти).

Приклад:

Ось як може виглядати одиниця NAL формату Annex B:

0x000001 65 ... (Закодовані відео дані)  
0x000001 41 ... (Закодовані відео дані)  
0x000001 67 ... (Дані SPS)
  • Стартовий код: позначає початок кожної одиниці NAL, що дозволяє декодеру легко визначити межі між одиницями.
  • Тип NAL: після стартового коду слідує число, що позначає тип NAL.
  • Використання у стрімінгу в реальному часі: Annex B ідеально підходить для сировинного стрімінгу, оскільки стартові коди гарантують, що кожен кадр буде декодований одразу після його отримання, без додаткових метаданих або контейнерів.

Формат Length-Prefixed

  • Замість стартових кодів кожна одиниця NAL передається з її довжиною (зазвичай 4 байти).
  • Зазвичай використовується в контейнерах, таких як MP4, які організовують і зберігають медіа у структурованому вигляді.

Приклад:

Ось як виглядатимуть дані з префіксом довжини:

0x0000000F ... (Довжина: 15 байтів даних NAL)  
0x00000012 ... (Довжина: 18 байтів даних NAL)

Контейнери

Контейнери, такі як MP4, MKV і AVI, використовуються для упаковки відео, аудіо та метаданих в один файл. Хоча вони корисні для зберігання та відтворення, вони додають накладні витрати, що не є ідеальними для стрімінгу в реальному часі.

3. Кадри в H.264: I, P та B кадри

H.264 досягає стиснення, кодувавши відео як послідовність кадрів, класифікованих на:

  • I-Frames (Інтракодовані кадри): Повні зображення, незалежні від інших кадрів. Важливі для пошуку та синхронізації. Великі за розміром порівняно з іншими кадрами.
  • P-Frames (Прогнозовані кадри): Кодують тільки різницю (дельту) від попереднього кадру. Менші за розміром, ідеальні для стрімінгу в реальному часі.
  • B-Frames (Двонаправлені кадри): Кодують різницю, використовуючи як минулі, так і майбутні кадри. Дуже ефективні, але вимагають буферизації, що робить їх менш підходящими для стрімінгу в реальному часі.

Розгляд стрімінгу в реальному часі:

  • P-Frames переважно використовуються для зменшення затримок і вимог до пропускної здатності.
  • B-Frames зазвичай уникаються в реальному часі, але є корисними для зберігання файлів, де ефективність стиснення важливіша за затримку.
    текст перекладу
    Чому формат Annex B для стрімінгу в реальному часі?

  • Стартові коди дозволяють декодування в реальному часі: Декодер не потребує парсингу додаткових метаданих для визначення меж кадрів.

  • Мала накладеність: Не потрібно використовувати контейнери, що робить потік легшим і швидшим.

  • Доставка кадрів по одному: Кожну одиницю NAL можна декодувати одразу після її отримання, забезпечуючи мінімальну затримку.

На відміну від цього, формат з префіксом довжини краще підходить для застосунків, де відео зберігається і відтворюється пізніше, оскільки він добре інтегрується з контейнерами.

Використовуючи формат Annex B і зосереджуючись на P-Frames для стрімінгу в реальному часі, ми досягаємо низької затримки і ефективної передачі відео з мінімальними вимогами до пропускної здатності. Цей підхід уникає складнощів з контейнерами, забезпечуючи плавне відтворення для застосунків в реальному часі.

FFmpeg: робоча конячка відео кодування

FFmpeg — це відкритий мультимедійний фреймворк, що широко використовується для обробки відео та аудіо. Він пропонує потужні можливості для кодування, декодування, транскодування, стрімінгу та багато іншого. В нашому випадку FFmpeg використовується для захоплення сирого відео з джерела (наприклад, KVM пристрою) та кодування його у високоефективний формат H.264 для стрімінгу.

Огляд важливих параметрів FFmpeg

Ось розбір критичних параметрів:

1. Preset: Контроль швидкості та стиснення

Параметр preset в FFmpeg дозволяє знайти баланс між швидкістю кодування та ефективністю стиснення.

Доступні налаштування:

  • ultrafast: Найшвидше кодування з мінімальним стисненням.
  • superfast, veryfast, faster, fast: Поступове зниження швидкості та стиснення.
  • medium (за замовчуванням): Збалансований підхід.
  • slow, slower, veryslow: Максимальне стиснення, але повільне кодування.

Чому ultrafast?

  • У стрімінгу в реальному часі швидкість критична для мінімізації затримки.
  • ultrafast зменшує обчислювальну складність, контролюючи такі фактори як:
    • GOP (Група картинок): Використовує більші розміри GOP, що кодує менше внутрішніх кадрів (I-frames).
    • Оцінка руху: Спрощує процес пошуку змін між кадрами.
    • Кодування ентропії: Використовує швидші алгоритми, такі як CAVLC, замість CABAC.

2. Tune: Забезпечення нульової затримки

Параметр -tune zerolatency є змінною для стрімінгу в реальному часі:

  • Стандартна поведінка: Кодування H.264 часто буферизує кілька кадрів перед початком кодування. Це ідеально для зберігання, де важливо досягти найкращого стиснення.
  • Нульова затримка: Видаляє цей етап буферизації.
    • Починає кодувати кадри одразу після їх надходження.
    • Забезпечує мінімальну затримку між захопленням відео і його передачею.

3. Профіль і рівень

  • Профіль (profile:v high): Визначає набір можливостей кодування H.264, які будуть використовуватися.
    • Baseline: Для малопотужних пристроїв і стрімінгу.
    • Main: Середня сумісність.
    • High: Максимальна ефективність і якість (наш вибір).
  • Рівень (level 4): Визначає обмеження по роздільній здатності, бітрейту та складності декодування.
    • Рівень 4 підтримує до 1080p відео при 30 FPS, що підходить для нашого використання.

4. Піксельний формат

  • Параметр pix_fmt yuv420p визначає колірний формат.

    • YUV420p: Ефективний формат з зменшеними кольоровими даними, широко підтримується для відео стрімінгу.
      текст перекладу
      Конфігурація виходу
  • Опція f h264 вказує на вихід у вигляді сирого потоку H.264 без контейнера (наприклад, MP4 або MKV).

  • Символ '-' в кінці перенаправляє закодований потік на стандартний вихід (консоль).

Як це вписується в наше застосування

  • Закодоване відео передається безпосередньо на консоль у форматі Annex B (з початковими кодами), що буде захоплене нашим Python бэкендом для подальшої обробки.
  • Використовуючи параметри ultrafast preset та zerolatency tuning, ми оптимізуємо швидкість і мінімізуємо затримки, що робить цей підхід ідеальним для стрімінгу відео з низькою затримкою.

Зрозумівши та налаштувавши ці параметри, ми забезпечуємо, щоб налаштування для стрімінгу були ефективними і швидкими, навіть у випадку обмежених мереж.

Розуміння ключових параметрів у відео кодуванні

Коли йдеться про ефективний стрімінг відео, певні параметри кодування відіграють критичну роль у контролі якості, швидкості та навантаженні на мережу. Одним із таких параметрів є GOP (Група картинок), що є важливим для балансування стиснення і ефективності відтворення. Давайте детальніше розглянемо, як це працює та як це підходить для застосувань стрімінгу в реальному часі.

Що таке GOP і як це працює в стрімінгу?

GOP, або Група картинок, визначає послідовність і структуру кадрів у відео потоці. Вона складається з різних типів кадрів, кожен з яких має свою мету:

  1. I-Frame (Внутрішній кадр):
  • Повне зображення, яке є самодостатнім і не залежить від інших кадрів для декодування.
  • Служить відправною точкою для декодування і також називається ключовим кадром.
  • Приклад використання: Коли глядач приєднується до прямого ефіру, процес декодування повинен початися з I-кадру, щоб ініціалізувати відтворення.

2. P-Frames (Прогнозовані кадри):

  • Містять тільки різниці (або дельти) від попереднього кадру.
  • Високоефективні для стрімінгу і займають менше місця.
  • Приклад: Якщо курсор миші зміщується на кілька пікселів між двома кадрами, P-кадр записує лише цю зміну, замість того, щоб надсилати все зображення.

3. B-Frames (Бідиирекційні кадри):

  • Використовують дані як з попередніх, так і з майбутніх кадрів для кодування змін.
  • Досягають максимального стиснення, але додають значну затримку.
  • Приклад: Ідеальні для оффлайн зберігання, як у файлах MP4, але не підходять для стрімінгу в реальному часі через залежність від майбутніх кадрів.

Як GOP контролює частоту кадрів?

GOP визначає, як часто ключові кадри (I-кадри) вставляються у відео потік, а також кількість кадрів дельт (P-кадрів) між ними. Цю структуру визначають:

  • Розмір GOP: Кількість кадрів у циклі GOP, наприклад, 30 кадрів.
  • Інтервал між ключовими кадрами: Визначає, як часто з’являється I-кадр, наприклад, кожні 2 секунди для потоку 30 FPS, що означає інтервал у 60 кадрів.

Чому це важливо?

Занадто мало I-кадрів:

  • Якщо ключові кадри з’являються занадто рідко, помилки від втрачених або пошкоджених кадрів можуть накопичуватися, що погіршує якість відео.
  • Це критично для стрімінгу в реальному часі, де кадри можуть бути втрачені через проблеми з мережею.

Занадто багато I-кадрів:

  • Збільшує використання пропускної здатності, оскільки I-кадри значно більші за P-кадри.
  • Приклад: I-кадр 1080p може важити 200–300 КБ, тоді як P-кадр може важити тільки 30–50 КБ.

Робочий процес стрімінгу і роль GOP

Коли починається прямий ефір:

  1. Метадані NAL-одиниць:
  • Потік починається з SPS (Sequence Parameter Set) та PPS (Picture Parameter Set) NAL-одиниць, які надають інформацію про роздільну здатність, профіль кодування та інші налаштування.
  • Ці пакети метаданих дозволяють декодеру розуміти вхідне відео.

2. Ключовий кадр (I-Frame):

Перший I-кадр відправляється, ініціюючи відтворення та служачи як опора для наступних кадрів.

3. Кадри дельт (P-Frames):

Далі йдуть P-кадри, які кодують лише зміни від попереднього кадру, щоб зменшити використання пропускної здатності.

4.
текст перекладу
**Періодичні ключові кадри:

Після певного інтервалу (контрольованого розміром GOP) відправляється ще один I-кадр, щоб скинути поширення помилок і дозволити новим глядачам приєднатися до потоку без затримок.

Як GOP балансує стиснення і якість?

Короткий GOP:

  • Більш часті I-кадри, що дозволяє краще відновлювати помилки і забезпечує більш плавне відтворення при втраті кадрів.
  • Приклад: Критично важливо для прямих трансляцій, де умови мережі змінюються.

Довгий GOP:

  • Зменшує використання пропускної здатності, збільшуючи кількість P-кадрів між I-кадрами.
  • Приклад: Підходить для стабільних мереж або відео з попереднім записом.

Для стрімінгу в реальному часі важливо знайти правильний розмір GOP. Типова конфігурація може включати 1 I-кадр, за яким слідують 29 P-кадрів для потоку 30 FPS, що дає розмір GOP рівно 30. Це забезпечує баланс між ефективністю стиснення і стійкістю до помилок.

Контролюючи структуру GOP, ми забезпечуємо, щоб стрімінг в реальному часі залишався ефективним, плавним і чуйним. У поєднанні з такими інструментами, як FFmpeg, і налаштованими параметрами, такими як -tune zerolatency, GOP стає основою стрімінгу з низькою затримкою.

Розуміння WebSockets та їх відмінності від HTTP

WebSockets — це сучасний протокол, призначений для двостороннього зв'язку в реальному часі між клієнтом і сервером. Вони відрізняються від традиційного протоколу HTTP як за функціональністю, так і за використанням.

Як працюють WebSockets

  1. Встановлення з'єднання:
  • WebSockets починаються з HTTP-хендшейку, але потім оновлюють з'єднання для двосторонньої комунікації.
  • На відміну від HTTP, який є безстанним і вимагає циклу запит-відповідь, WebSockets дозволяють безперервний обмін даними без повторних хендшейків.

2. Повнодуплексна комунікація:

  • WebSockets дозволяють одночасну передачу даних в обох напрямках, що робить їх ідеальними для додатків реального часу, таких як чати, прямі трансляції чи колаборативні інструменти.

Як WebSockets відрізняються від HTTP

Тип з'єднання:

  • WebSockets: Постійне, єдине з'єднання
  • HTTP: Короткочасні, безстанні запити

Потік даних:

  • WebSockets: Повнодуплексна (двостороння комунікація)
  • HTTP: Напівдуплексна (модель запит-відповідь)

Продуктивність:

  • WebSockets: Ефективні для оновлень в реальному часі
  • HTTP: Навантаження через повторні хендшейки

Використання:

  • WebSockets: Додатки в реальному часі (наприклад, чати, ігри)
  • HTTP: Додатки запит-відповідь (наприклад, веб-сторінки)

Протокол:

  • WebSockets: Оновлення з HTTP до WebSocket
  • HTTP: Чисто HTTP-комунікація

Підтримка WebSocket у FastAPI

FastAPI надає вбудовану підтримку WebSockets, що дозволяє розробникам легко створювати функціональність в реальному часі. Ось короткий огляд того, як це працює:

  1. Базова точка WebSocket:
  • FastAPI пропонує спеціальний тип WebSocket для обробки WebSocket з'єднань.
from fastapi import FastAPI, WebSocket  

app = FastAPI()  

@app.websocket("/ws")  
async def websocket_endpoint(websocket: WebSocket):  
 await websocket.accept()  
 while True:  
 data = await websocket.receive_text()  
 await websocket.send_text(f"Message received: {data}")

2. Функції в реальному часі з WebSockets у FastAPI:

Ідеально підходить для таких додатків, як:

  • Чати або сповіщення
  • Колаборативні інструменти
  • Контроль сигналів для стрімінгу в реальному часі

Інтегруючи WebSockets, FastAPI дозволяє розробникам легко створювати ефективні, з низькою затримкою додатки в реальному часі.

Додаток для відео стрімінгу з низькою затримкою: Кодування H.264 з WebSockets і WebCodecs.

FastApi Backend

Nal типи:

H.264 Network Abstraction Layer (NAL) одиниці є основними будівельними блоками відео потоків H.264. Кожна NAL одиниця містить конкретні дані для декодування та відтворення відео. Ось короткий огляд основних типів NAL одиниць та їх призначення:

Основні типи NAL одиниць

  1. SPS (Набір параметрів послідовності)
  • Містить важливі метадані, що описують загальну послідовність відео, такі як роздільна здатність, частота кадрів і профіль кодування.
  • Необхідний для ініціалізації декодера.
  • Зазвичай надсилається на початку потоку.
  • NAL ID: 7

2.
текст перекладу
**PPS (Набір параметрів зображення)

  • Містить додаткову інформацію для декодування окремих зображень, таких як режим ентропійного кодування або налаштування групи слайсів.
  • Тісно працює з SPS для керування декодуванням кадрів.
  • NAL ID: 8

3. SEI (Додаткова інформація для поліпшення)

  • Містить допоміжні дані, такі як часові мітки, колірна інформація або субтитри.
  • Не є обов'язковою для декодування відео, але може покращити відтворення.
  • NAL ID: 6

4. IDR (Моментальний скидання декодера)

  • Спеціальний тип I-кадру (ключовий кадр).
  • Вказує точку, з якої можна почати декодування незалежно.
  • Використовується для скидання декодування і усунення помилок попередніх кадрів.
  • NAL ID: 5

5. Non-IDR Slice (P-кадр)

  • Містить дані для предсказуваного кодування, використовуючи кадри-референції (попередні кадри).
  • Кодує лише зміни (дельту) від референтного кадру, що заощаджує пропускну здатність.
  • Для живого стрімінгу B-кадри не включаються.
  • NAL ID: 1

Перспектива декодера щодо кадрів

З точки зору декодера всі кадри можна класифікувати на два типи: Ключові кадри і Дельта-кадри:

  1. Ключові кадри
  • Ці кадри є самостійними і можуть бути декодовані без залежності від інших кадрів.
  • В H.264 ключовий кадр складається з комбінації SPS, PPS, SEI та IDR NAL одиниць.
  • Вони виступають як точки синхронізації і допомагають скинути процес декодування при необхідності.

2. Дельта-кадри

  • Ці кадри кодують лише зміни (дельти) порівняно з референтними кадрами (ключовими або дельта-кадрами).
  • В H.264 Non-IDR NAL одиниця є представленням дельта-кадру.

Чому це важливо?

  • Ключові кадри гарантують, що декодер може почати декодування або відновити процес після помилки в будь-який момент стріму.
  • Дельта-кадри оптимізують використання пропускної здатності, передаючи лише інкрементальні зміни, що робить відео стрімінг в реальному часі ефективним.

Ця класифікація є основою того, як відео стрімінгові додатки керують стисненням, передачею і відтворенням.

Архітектура послідовності кадрів FFmpeg

Архітектура вихідного потоку FFmpeg характеризується повторюваним патерном кадрів. Послідовність починається з ключового кадру, що складається з даних типу NAL, таких як SPS, PPS, SEI і IDR, які повинні бути зібрані і передані клієнту як один кадр. Далі йде серія дельта-кадрів, що складаються з даних NAL типу non-IDR. Послідовність потім повторюється, починаючи з опціонального SEI (необхідного лише на початку потоку), після чого йдуть SPS, PPS і IDR NAL типи даних, а потім ще серія non-IDR дельта-кадрів. Циклічний патерн триває протягом всього вихідного потоку.

pic

@APP.websocket("/websocket/kvm-stream")  
async def stream_kvm(websocket: WebSocket) -> Optional[bytes]:  
 """KVM стрімінг з кодуванням h264.  

 Ця функція створить конвеєр FFMPEG для зчитування кадрів з KVM пристрою через USB,  
 їх кодування в формат h264 Annexure B та зчитування вихідних байтів через pipe subprocess  
 і парсинг їх для створення окремих NAL даних та відправлення їх до клієнта.  

 Параметри:  
 websocket (WebSocket): Об'єкт WebSocket для встановлення з'єднання.  

 Повертає:  
 Optional[bytes]: Закодовані відео дані.  
 """  
 await websocket.accept()  

 kvm_device_name = "video_capture_device"  

 if not kvm_device_name:  
 BACKEND_LOGGER.error("KVM не знайдений, закриваємо з'єднання.")  
 await websocket.close()  
 return  

 ffmpeg_executable_location = os.path.join("D:", "my_software",  
 "ffmpeg", "ffmpeg-static-win64-gpl",  
 "bin", "ffmpeg.exe")  

 ffmpeg_command = [  
 ffmpeg_executable_location,  
 '-f', 'dshow',  
 '-i', 'video=' + kvm_device_name,  
 '-s', '1920x1080',  
 '-c:v', 'libx264',  
 '-preset', 'ultrafast',  
 '-tune', 'zerolatency',  
 '-profile:v', 'high',  
 '-level', '4',  
 '-pix_fmt', 'yuv420p',  
 '-f', 'h264',  
 '-'  
 ]  

 # Запуск процесу FFMPEG.

текст перекладу
try:  
 encoder_process_interface = await asyncio.create_subprocess_exec(  
 *ffmpeg_command,  
 stdout=subprocess.PIPE,  
 stderr=subprocess.DEVNULL,  
 bufsize=0)  
 except FileNotFoundError:  
 BACKEND_LOGGER.error(f"FFMPEG виконуваний файл не знайдено за адресою: {ffmpeg_executable_location}")  
 BACKEND_LOGGER.error("Вихід з процесу KVM стріму.")  
 await websocket.close()  
 return  
 except Exception as error:  
 BACKEND_LOGGER.error(f"Сталася несподівана помилка: {error}")  
 BACKEND_LOGGER.error("Вихід з процесу KVM стріму.")  
 await websocket.close()  
 return  

 BACKEND_LOGGER.info(f"Процес FFMPEG для кодування KVM стріму був "  
 f"запущений з ID процесу: {encoder_process_interface.pid}")  

 complete_frame_data: bytes = b""  

 # Визначаємо шаблон регулярного виразу.  
 nal_start_code_pattern: Pattern[bytes] = re.compile(b'(?:\x00\x00\x00\x01)')  

 while True:  
 try:  
 # Читаємо 300 КБ (307200 байт) даних, це означає, що буде прочитано максимум 300 КБ, якщо менше даних  
 # доступно, то буде повернено лише доступні дані, без очікування  
 # на повні 300 КБ. Це число ґрунтується на спробах і помилках,  
 # максимальний розмір IDR кадру був близько 280 КБ.  
 new_encoded_data: bytes = await encoder_process_interface.stdout.read(307200)  
 except Exception as error:  
 BACKEND_LOGGER.error("Помилка при читанні з конвеєра процесу FFMPEG.")  
 BACKEND_LOGGER.error(error)  
 BACKEND_LOGGER.error("Вихід з KVM стріму.")  
 break  

 if not new_encoded_data:  
 BACKEND_LOGGER.error("Немає даних від процесу FFMPEG, вихід зі стріму.")  
 break  

 # Знаходимо всі неперекриваючіся збіги.  
 nal_matches_info: list = list(nal_start_code_pattern.finditer(new_encoded_data))  

 no_of_nal_units: int = len(nal_matches_info)  

 # Якщо не було знайдено жодного збігу, це означає, що поточний шматок даних має проміжні  
 # байти, які продовжуються з попередніх даних, тому їх слід просто додати до  
 # існуючих nal даних і пропустити решту коду.  
 if not nal_matches_info:  
 complete_frame_data += new_encoded_data  
 continue  

 for match_index, match in enumerate(nal_matches_info):  
 nal_type: int = new_encoded_data[match.end()] & 0x1F  

 # Якщо перший збіг у списку збігів відбувається не в першому індексі,  
 # тоді перші кілька байт (до початку першого збігу) будуть відповідати  
 # даним з попередньої nal одиниці, тому ми повинні додати їх до  
 # існуючих nal даних, а потім відправити клієнту і очистити буфер.  
 if match_index == 0 and match.start() != 0:  
 complete_frame_data += new_encoded_data[: match.start()]  

 if complete_frame_data:  
 # Відправляємо тільки, якщо поточна nal одиниця є Non-Idr, оскільки інші дані, такі як SPS, PPS,  
 # SEI, IDR фактично надходять як окремі шматки, нам потрібно їх об'єднати перед  
 # відправкою. Реалізація враховує їх об'єднання, але це відбувається тільки тоді,  
 # коли Non-Idr кадр йде після ключового кадру (який є комбінацією SPS, PPS,  
 # SEI, IDR).

текст перекладу
try:  
 encoder_process_interface = await asyncio.create_subprocess_exec(  
 *ffmpeg_command,  
 stdout=subprocess.PIPE,  
 stderr=subprocess.DEVNULL,  
 bufsize=0)  
 except FileNotFoundError:  
 BACKEND_LOGGER.error(f"Не знайдено виконуваний файл FFMPEG за адресою: {ffmpeg_executable_location}")  
 BACKEND_LOGGER.error("Завершення процесу KVM стріму.")  
 await websocket.close()  
 return  
 except Exception as error:  
 BACKEND_LOGGER.error(f"Сталася несподівана помилка: {error}")  
 BACKEND_LOGGER.error("Завершення процесу KVM стріму.")  
 await websocket.close()  
 return  

 BACKEND_LOGGER.info(f"Процес FFMPEG для кодування KVM стріму було "  
 f"запущено з ID процесу: {encoder_process_interface.pid}")  

 complete_frame_data: bytes = b""  

 # Визначення шаблону регулярного виразу.  
 nal_start_code_pattern: Pattern[bytes] = re.compile(b'(?:\x00\x00\x00\x01)')  

 while True:  
 try:  
 # Читання 300 КБ (307200 байт) даних, це означає, що буде прочитано максимум 300 КБ, якщо менше даних  
 # доступно, то буде повернено лише доступні дані, без очікування  
 # на повні 300 КБ. Це число ґрунтується на спробах і помилках,  
 # максимальний розмір IDR кадру був близько 280 КБ.  
 new_encoded_data: bytes = await encoder_process_interface.stdout.read(307200)  
 except Exception as error:  
 BACKEND_LOGGER.error("Помилка при читанні з конвеєра процесу FFMPEG.")  
 BACKEND_LOGGER.error(error)  
 BACKEND_LOGGER.error("Вихід з KVM стріму.")  
 break  

 if not new_encoded_data:  
 BACKEND_LOGGER.error("Немає даних від процесу FFMPEG, вихід зі стріму.")  
 break  

 # Знаходимо всі неперекриваючіся збіги.  
 nal_matches_info: list = list(nal_start_code_pattern.finditer(new_encoded_data))  

 no_of_nal_units: int = len(nal_matches_info)  

 # Якщо не було знайдено жодного збігу, це означає, що поточний шматок даних має проміжні  
 # байти, які продовжуються з попередніх даних, тому їх слід просто додати до  
 # існуючих nal даних і пропустити решту коду.  
 if not nal_matches_info:  
 complete_frame_data += new_encoded_data  
 continue  

 for match_index, match in enumerate(nal_matches_info):  
 nal_type: int = new_encoded_data[match.end()] & 0x1F  

 # Якщо перший збіг у списку збігів відбувається не в першому індексі,  
 # тоді перші кілька байт (до початку першого збігу) будуть відповідати  
 # даним з попередньої nal одиниці, тому ми повинні додати їх до  
 # існуючих nal даних, а потім відправити клієнту і очистити буфер.  
 if match_index == 0 and match.start() != 0:  
 complete_frame_data += new_encoded_data[: match.start()]  

 if complete_frame_data:  
 # Відправляємо тільки, якщо поточна nal одиниця є Non-Idr, оскільки інші дані, такі як SPS, PPS,  
 # SEI, IDR фактично надходять як окремі шматки, нам потрібно їх об'єднати перед  
 # відправкою. Реалізація враховує їх об'єднання, але це відбувається тільки тоді,  
 # коли Non-Idr кадр йде після ключового кадру (який є комбінацією SPS, PPS,  
 # SEI, IDR).

текст перекладу
**Парсинг та обробка NAL одиниць:**

- Відеодані H.264 складаються з NAL (Network Abstraction Layer) одиниць, кожна з яких має префікс стартового коду (`\x00\x00\x00\x01`).
- Код використовує регулярний вираз для ідентифікації NAL одиниць і обробляє їх таким чином:
- Якщо перші байти є частиною неповної NAL одиниці, вони додаються до попереднього буфера кадрів (`complete_frame_data`).
- Завершений буфер кадрів надсилається клієнту, якщо це кадр без IDR (delta кадр).
- Це забезпечує правильний парсинг та послідовне стрімінгування кадрів.

**4. Управління кадрами та стрімінг:**

- Цикл стрімінгу безперервно читає дані з `stdout` FFmpeg порціями (300 КБ, визначено експериментальним шляхом на основі максимального розміру IDR кадру).
- Дані розбираються на окремі NAL одиниці, які потім передаються клієнту.
- Використання `sleep(0)` забезпечує передачу контролю до циклу подій, щоб запобігти переповненню буфера і регулювати частоту кадрів.

**5. Обробка помилок та очищення:**

- Код містить надійну обробку помилок для закриття WebSocket підключення та коректного завершення процесу FFmpeg у разі проблем.
- Різні ситуації, такі як відключення клієнта, помилки при читанні чи запису даних і помилки процесу FFmpeg, управляються.

## Декодування відео кадрів на фронтенді: WebCodecs та Web Workers

У цьому розділі ми зосередимося на реалізації фронтенду нашого додатку для відео стрімінгу з низькою затримкою. Метою є ефективне декодування відео кадрів, закодованих у форматі H.264, отриманих через WebSocket, та плавне їх відображення у браузері. Це досягається за допомогою двох ключових технологій: **WebCodecs API** та **Web Workers**.

## WebCodecs API: Сучасний декодер для високопродуктивного відео

**WebCodecs API** — це низькорівневий API, який забезпечує прямий доступ до відео та аудіо декодерів, енкодерів та можливостей обробки сирих кадрів. Він надзвичайно ефективний, призначений для реального часу, таких як відео стрімінг, і усуває непотрібні накладні витрати, працюючи безпосередньо з медіа-потоками. Основні переваги:

- **Низька затримка**: Кадри декодуються одразу після їх отримання, без додаткового буферизування.
- **Апаратне прискорення**: Використовує GPU для забезпечення плавного відтворення навіть для відео високої роздільної здатності.
- **Підтримка сучасних форматів**: WebCodecs працює бездоганно з H.264, AV1, VP9 та іншими.

## Web Workers: Підтримка відгуку головного потоку

Для забезпечення безперервної роботи користувача використовуються **Web Workers** для обробки процесу декодування. Делегуючи обчислювальні завдання, пов'язані з декодуванням, на окремий потік, ми:

1. Запобігаємо **блокуванню головного потоку**, забезпечуючи плавну взаємодію з користувачем.
2. Підтримуємо постійне відтворення кадрів без ривків, навіть при високому навантаженні.
3. Використовуємо багатоядерні процесори для ефективної обробки.

Web Workers в нашому додатку будуть обробляти:

1. Отримання закодованих кадрів від головного потоку.
2. Декодування цих кадрів за допомогою WebCodecs.
3. Надсилання декодованих кадрів назад для відображення.

Поєднуючи **WebCodecs** і **Web Workers**, ми можемо досягти високої чуйності та ефективності на фронтенді, здатного декодувати та відображати відео кадри в реальному часі. У наступному розділі буде показано повну реалізацію, яка демонструє, як ці технології працюють разом.

## Пояснення коду відео декодера (Web Worker)

У цьому розділі ми розглянемо код, відповідальний за декодування відео кадрів на фронтенді, який виконується всередині **Web Worker**. Це дозволяє процесу декодування виконуватися в окремому потоці від головного, зберігаючи плавність роботи додатку.
текст перекладу
**Константи та змінні**:

- `SAMPLE_DATA_INDEX_START` і `SAMPLE_DATA_INDEX_END`: Ці константи визначають індекси байтів вхідних даних NAL одиниці, які нас цікавлять. Ми фокусуємось на перших 6 байтах, що допомагає витягти тип NAL одиниці.
- `NAL_TYPE_INDICATOR_INDEX`: Позиція індексу в NAL одиниці, де вказано тип кадру.
- `NAL_TYPE_MASK`: Бітова маска (`0x1F`), яка використовується для витягнення останніх 5 бітів з NAL одиниці, що представляють тип кадру (ключовий кадр або кадр зміни).
- `DELTA_FRAME_ID`: Значення (`1`), яке представляє кадр зміни (delta frame), використовується для відрізнення ключових кадрів від кадрів зміни.

**2. Ініціалізація (`initializeDecoder`)**:

- Створюється екземпляр **VideoDecoder**, який обробляє декодування відеоданих. Конфігурація декодера вказує, що вхідний формат даних — це "Annex B" (сирий формат H.264 NAL одиниці).
- Декодер конфігурується з кодеком (`avc1.42E01E`), який представляє H.264 (Advanced Video Coding), і оптимізується для **низької затримки** декодування. Це критично важливо для реальних застосунків відео стрімінгу.
- Якщо декодер стикається з помилкою, він надсилає повідомлення назад до основного потоку, що дозволяє нам обробити проблему коректно.
текст перекладу
Аналогічно, після того як кадр буде декодований, він відправляється назад до основного потоку для відображення.

**3. Обробка відео даних (`processVideoData`)**:

- Цей метод відповідає за отримання закодованих відео даних (H.264 NAL одиниць) від основного потоку та їх обробку.
- Перші 6 байтів NAL одиниці витягуються для визначення типу NAL одиниці за допомогою побітової операції (`NAL_TYPE_MASK`).
- Залежно від типу NAL одиниці, метод визначає, чи є кадр **кадром зміни** (кадр, який зберігає лише зміни від попереднього кадру) чи **ключовим кадром** (I-кадр, який є самодостатнім і може бути декодований незалежно).
- Потім створюється екземпляр `EncodedVideoChunk` з витягнутими даними та відповідним часовим штампом (який обчислюється на основі частоти кадрів 60fps). Цей шматок передається декодеру для декодування.
- Часовий штамп збільшується після кожного кадру для забезпечення належного синхронізування та часу відтворення.

**4. Прослуховувач подій (`self.onmessage`)**:

- Веб-робітник слухає повідомлення від основного потоку. Ці повідомлення ініціалізують декодер (`init` повідомлення) або надсилають відео дані (`videoData` повідомлення) для декодування.
- Після отримання повідомлення `init` ініціалізується декодер.
- Після отримання повідомлення `videoData` робітник обробляє вхідні дані та декодує їх.

## Потік обробки відео даних:

1. Основний потік надсилає повідомлення `init` для ініціалізації декодера.
2. Робітник декодує вхідні кадри по черзі.

- Для кожного кадру робітник витягує тип NAL (ключовий кадр чи кадр зміни).
- Створюється відповідний `EncodedVideoChunk` з даними та часовим штампом.
- Шматок передається екземпляру **VideoDecoder** для декодування.

3. Після декодування кадр відправляється назад до основного потоку через `self.postMessage()`.

4. Основний потік потім відображає декодований кадр.

## Компонент H264VideoRenderer у Vue

Цей розділ надає код нового компонента, який відображає відео потік.


текст перекладу
getContext("2d");  

 };  

 kvmDisplaySocket.onmessage = (event) => {  

 decoderWorker.postMessage(  
 {data: event.data,  
 type: "videoData"},  
 [event.data]  
 );  

 };  

 kvmDisplaySocket.onclose = () => {  

 decoderWorker.terminate();  
 handleImageError();  

 };  

 kvmDisplaySocket.onerror = (event) => {  

 console.error(  
 "Сталася помилка:",  
 event  
 );  

 };  

 isLoading.value = false;  

});  

onBeforeUnmount(() => {  

 decoderWorker.terminate();  

 isLoading.value = true;  

});  

/*  
<---------- Функція рендерингу KVM ---------->  
*/  

// Дані  

// Реактивні дані  

const defaultVideoWidth = 1020;  
const defaultVideoHeight = 720;  

const kvmContainerParentWidth = ref(defaultVideoWidth);  
const kvmContainerParentHeight = ref(defaultVideoHeight);  

// Методи  

// Утиліти  

/**  
 * Відображає KVM за допомогою події декодування.  
 *   
 * @param {Object} decoderEvent - Дані події декодування.  
 * @param {Object} [decoderEvent.data.frame] - Об'єкт кадру.  
 * @param {string} [decoderEvent.data.message] - Повідомлення в журналі.  
 * @returns {void}  
 */  
const renderKvmDisplay = (decoderEvent) => {  

 if (decoderEvent.data.type === "frame") {  

 kvmComponent.drawImage(  
 decoderEvent.data.frame,   
 SRC_IMG_X_OFFSET,   
 SRC_IMG_Y_OFFSET,   
 decoderEvent.data.frame.displayWidth,   
 decoderEvent.data.frame.displayHeight,  
 DST_IMG_X_OFFSET,   
 DST_IMG_Y_OFFSET,   
 kvmContainerParentWidth.value,   
 kvmContainerParentHeight.value  
 );  

 /**  
 * Закриває кадр, щоб звільнити ресурси.  
 */  
 decoderEvent.data.frame.close();  

 } else if (decoderEvent.data.type === "log") {  

 /**  
 * Логує повідомлення в консоль.  
 */  
 console.log(decoderEvent.data.message);  

 }  

};  

// Життєвий цикл  

onMounted(() => {  

 decoderWorker.postMessage({type: "init"});  

 // Обробка повідомлень від робітника  
 decoderWorker.onmessage = renderKvmDisplay;   

});  

onBeforeUnmount(() => {  

 if (kvmDisplaySocket && kvmDisplaySocket.readyState === WebSocket.OPEN) {  

 kvmDisplaySocket.close();  

 }   
 decoderWorker.terminate();  

});  






Детальне пояснення компонента H264VideoRenderer.vue

Цей компонент Vue, H264VideoRenderer.vue, займається рендерингом потоку KVM (клавіатура-відео-миша). Він управляє декодуванням відео за допомогою Web Worker, комунікацією через WebSocket для отримання відео даних і відображенням декодованих кадрів на полотні. Ось розбір коду:

Налаштування скрипту

Імпорти

  • onBeforeMount, onBeforeUnmount, onMounted, ref з Vue: Це хуки життєвого циклу та допоміжні функції реактивних посилань з Composition API Vue 3.
  • VideoFrameDecoderWorker з файлу @/scripts/videoFrameDecoderWorker?worker: Це імпортує Web Worker, який буде обробляти декодування кадрів відео у фоновому режимі.
    текст перекладу
    ?worker запит вказує Webpack, щоб обробляти це як імпорт Web Worker.

Реактивні та нерекативні дані

  • isKvmLoaded: Реактивне посилання, яке відслідковує, чи успішно завантажено зображення з KVM потоку.
  • kvmComponent: Нерекативне посилання на 2D контекст малювання елемента canvas, яке використовується для рендерингу кадрів.
  • decoderWorker: Імпортований Web Worker, який буде обробляти декодування відеокадрів.
  • SRC_IMG_X_OFFSET, SRC_IMG_Y_OFFSET, DST_IMG_X_OFFSET, DST_IMG_Y_OFFSET: Зсуви для малювання зображення на полотні. Ці значення встановлені в нуль, але можуть бути відкориговані при потребі для позиціонування.

Обробники подій

  • handleImageLoad: Ця функція оновлює isKvmLoaded на true, коли зображення з KVM потоку успішно завантажено.
  • handleImageError: Ця функція встановлює isKvmLoaded на false, коли зображення з KVM потоку не вдалося завантажити (наприклад, коли KVM потік недоступний).

Налаштування WebSocket (Комунікація з бекендом)

  • kvmDisplaySocket: Встановлюється WebSocket з'єднання з бекенд-сервісом, який надає KVM потік. WebSocket слухає відеодані (у форматі ArrayBuffer) і передає їх Web Worker для декодування.

Події WebSocket

  • onopen: Коли WebSocket з'єднання встановлено, викликається функція handleImageLoad, і 2D контекст для полотна ініціалізується.
  • onmessage: Коли WebSocket отримує дані, він передає повідомлення Web Worker (decoderWorker) з відеоданими для обробки.
  • onclose: Якщо WebSocket з'єднання закрите, Web Worker завершено, і викликається handleImageError.
  • onerror: Логує будь-які помилки, які виникають під час комунікації через WebSocket.

URL WebSocket:

URL WebSocket для KVM потоку: ws://localhost:5566/websocket/kvm-strean. Цей URL з'єднується з WebSocket кінцевою точкою на бекенді.

Життєві цикли

onBeforeMount:

  • Цей хук виконується до монтування компонента. Він ініціалізує WebSocket з'єднання та налаштовує слухачів подій для комунікації через WebSocket.
  • Встановлюється з'єднання WebSocket з бекендом для отримання відеокадрів, а kvmComponent готується для рендерингу.
  • Web Worker (decoderWorker) використовується для обробки відеоданих при їх надходженні.

onBeforeUnmount:

  • Цей хук виконується перед демонтажем компонента. Він завершує Web Worker і закриває WebSocket з'єднання для належного очищення, коли компонент більше не потрібен.

Рендеринг KVM

  • defaultVideoWidth і defaultVideoHeight: За умовчанням розміри відео полотна, встановлені на 1020x720 пікселів.
  • kvmContainerParentWidth і kvmContainerParentHeight: Ці реактивні посилання відслідковують розміри батьківського контейнера для KVM полотна.

renderKvmDisplay:

Ця методика відповідає за рендеринг декодованих відеокадрів на полотні. Вона слухає повідомлення про декодований кадр від Web Worker.

  • Рендеринг кадрів: Коли Web Worker надсилає кадр (тип "frame"), викликається функція drawImage для відображення кадру на полотні.
    текст перекладу
    Кадр малюється з координатами джерела та призначення (з зсувами), а displayWidth та displayHeight кадру використовуються для його масштабування, щоб він підходив до полотна.
  • Обробка журналу: Якщо Web Worker надсилає повідомлення журналу (тип "log"), воно реєструється в консолі для налагодження.
  • Управління пам'яттю: Після рендерингу кадру викликається frame.close(), щоб звільнити ресурси, пов'язані з кадром.

Хук життєвого циклу onMounted

onMounted:

  • Коли компонент змонтовано, він надсилає повідомлення "init" до Web Worker для ініціалізації декодувальника.
  • Також налаштовується обробник onmessage для Web Worker, щоб викликати метод renderKvmDisplay щоразу, коли кадр буде декодовано.

Завантаження/Розмонтування

onBeforeUnmount:

  • Цей хук забезпечує належне завершення роботи як WebSocket з'єднання, так і Web Worker перед тим, як компонент буде видалено з DOM.

Структура шаблону

Відображення Canvas і зображення

  • Шаблон складається з контейнера div з двома можливими дочірніми елементами:

  • : Це полотно відображає KVM потік, коли isKvmLoaded дорівнює true. width та height прив'язані до kvmContainerParentWidth та kvmContainerParentHeight відповідно.
  • `: ЯкщоisKvmLoadedдорівнюєfalse, відображається це зображення, яке вказує на те, що KVM потік не завантажено.srcвстановлюється на@/assets/backendnotconnected.svg`, що є заповнювачем, який вказує, що бекенд KVM потік недоступний.

Стилі

  • scoped: Розділ стилів порожній в цьому прикладі, але атрибут scoped гарантує, що будь-які стилі, визначені тут, застосовуватимуться тільки до цього компонента.

Основні висновки

  1. WebSocket: Компонент використовує WebSocket з'єднання для отримання бінарних даних (відеокадрів) від бекенд-сервісу.
  2. Web Worker: Web Worker використовується для передачі завдання декодування відеокадрів, що покращує продуктивність, зберігаючи головний потік UI вільним.
  3. Рендеринг на Canvas: Декодовані кадри малюються на елементі canvas, який масштабує та відображає кадри.
  4. Життєвий цикл компонента: Правильне керування життєвим циклом забезпечує належне очищення WebSocket з'єднання та Web Worker, коли компонент видаляється.

Ця установка забезпечує ефективне оброблення декодування та рендерингу відео, навіть для додатків з високими вимогами до продуктивності, таких як KVM стрімінг, де низька затримка та висока чутливість до змін критичні.

Інтеграція H264VideoRenderer у HomeView.vue

        ```  
## Порівняння продуктивності та вимоги до пропускної здатності

Під час тестування продуктивності нашого рішення на основі H.264, результати були _вражаюче схожі на арахіс_! 🍿 Давайте розглянемо деталі:

## Наївний підхід (JPEG + HTTP Multipart)

У нашій початковій спробі ми надсилали JPEG-кодовані кадри через HTTP multipart з'єднання. Кожен кадр важив приблизно **240-250 КБ**, що означає, що для плавного потоку на 30 FPS:

**240 КБ × 30 = 7200 КБ за секунду**, або приблизно **7,2 МБ/с**.

Це _дуже багато_ арахісу для перенесення в одному мішку.

## Оптимізований підхід (H.264 з ключовими кадрами та кадрами різниць)

З нашим новим підходом, ключовий кадр все ще важить близько **240-250 КБ**, але кадри різниць (магічні компактні кадри) в середньому складають лише **0,01 КБ**. Давайте порахуємо для 30 FPS:

- 1 ключовий кадр = **250 КБ**
- 29 кадрів різниць = **29 × 0,01 КБ ≈ 0,29 КБ**
- Загальна вимога до пропускної здатності за секунду = **250 + 0,29 = 250,29 КБ**

Це приблизно **0,25 МБ/с**, порівняно з попередніми **7,2 МБ/с**. Це _в 28 разів менше пропускної здатності!_ 🎉

## Що це означає?

- Наївний підхід — це як продавець арахісу, який несе **7,2 МБ арахісу за секунду** до вашого столу.
текст перекладу
Ефективно? Не зовсім.
- Підхід H.264 доставляє тільки _дельту арахісу_ (хрусткий і крихітний), вимагаючи лише **0,25 МБ на секунду**.

Інакше кажучи, ми перейшли від **перевантаження арахісом** до ідеально збалансованого пакету закусок! 🥜

## Погляд на фінальний результат

Ми прикріпили відео.gif фінального виходу потоку для вашого задоволення🍞✨

![pic](https://drive.javascript.org.ua/f8aa27f78f1_w6xQGSGlzmZLABV8FdqMzQ_gif)

## Останні слова: Арахісовий Дякую

Якщо ви прочитали до цього моменту, ой, ви або:

1. Ентузіаст стрімінгу, який шукає рішення для реальних проблем.
2. Хтось, хто просто _дуже_ любить арахіс і метафори про стрімінг. 🥜

У будь-якому разі, я сподіваюся, що ця подорож по світу оптимізованого відео стрімінгу принесла вам кілька посмішок і користі для вашого дня! Якщо ви все ще тримаєтесь за той 30-FPS JPEG потік, як за вперте арахісове шкаралупе, **час оновити свій набір трюків.**

**Дякую за прочитання, і нехай ваші потоки завжди будуть плавними та легкими на пропускну здатність!**

கேடில் விழுச்செல்வம் கல்வி யொருவற்கு மாடல்ல மற்றை யவை.

Серед усіх форм багатства, багатство знань є найвищим,  
Всі інші види багатства є тимчасовими.

— — **_Тіруваллувар_**, великий тамільський мудрець.

[

## GitHub - Praveenstein/low-latency-streamer: Рішення для відео стрімінгу з низькою затримкою за допомогою кодування H.264…

### Рішення для відео стрімінгу з низькою затримкою за допомогою кодування H.264, WebCodecs API і Web Workers для плавного та ефективного…

github.com

](https://github.com/Praveenstein/low-latency-streamer?source=post_page-----4c69f5124fda--------------------------------)



Перекладено з: [Low Latency Video Streaming Application with FastApi](https://medium.com/@praveen06061995/low-latency-video-streaming-application-with-fastapi-4c69f5124fda)

Leave a Reply

Your email address will not be published. Required fields are marked *