
Эта статья открывает цикл публикаций о создании open-source веб-приложения для стриминга видео с веб-камеры и управления роботом. Приложение позволит транслировать видео с камеры в реальном времени и отправлять команды управления роботом через интерфейс. Думаю, статья будет интересна веб-программистам, интересующимся работой с видеостримингом и FastAPI, а также робототехникам и энтузиастам DIY-проектов.
Оглавление
Введение
Веб-камера — глаза робота. Пишу веб-приложение на FastAPI для управления DIY-проектом
Инфраструктура и развёртывание проекта
Идея проекта возникла из моего интереса к робототехнике и веб-программированию. Ранее в статье DIY-проект: гусеничная платформа с ИК-управлением на Arduino я создал гусеничную платформу на базе Iscra mini, управляемую ИК-пультом, и захотел развить эту платформу.
В качестве камеры я планирую использовать экшн-камеру, которая может работать как веб-камера. Если она окажется несовместимой с Linux, её можно будет заменить обычной веб-камерой. Основная цель проекта — создать гибкое решение, которое будет полезным для разных DIY-проектов.
Стриминг видео с веб-камеры
Я начал эксперименты со стримингом видео, используя универсальные UVC-драйверы в Linux Debian. UVC (USB Video Class) — это стандарт, разработанный USB Implementers Forum (USB-IF), который определяет, как веб-камеры, цифровые видеокамеры или другие видеоустройства должны передавать видеопоток через USB. Универсальные драйверы добавят удобства определения в системе разных веб-камер.
Проще говоря, это программное обеспечение в операционной системе (в моём случае в Linux), которое обеспечивает взаимодействие между ядром и UVC-совместимыми устройствами. В Linux этот драйвер называется uvcvideo и встроен в ядро как часть подсистемы Video4Linux2 (V4L2).
Для стриминга видео с камеры я планирую использовать программу MJPG-Streamer — готовое решение для трансляции видео по HTTP. Само веб-приложение будет подключаться к нему по URL. Использование этой программы сократит время на разработку, экономя его на механизме трансляции видеопотока.
Теперь я установлю MJPG-Streamer и все необходимые зависимости на целевое устройство. Сейчас я тестирую на ПК с Debian, однако этот процесс почти идентичен установке для Raspbian или Arbian, которые работают на платах Raspberry Pi и Orange Pi (в будущем я буду использовать такие платы для постройки робота в статьях DIY).
Шаг 1: Обновление системы
Обновляю список пакетов и устанавливаю их:
sudo apt-get update sudo apt-get upgrade -y
Эти команды нужны для предотвращения проблем с совместимостью пакетов.
Шаг 2: Установка зависимостей
Теперь устанавливаю зависимости:
sudo apt-get install -y build-essential cmake libjpeg-dev libv4l-dev ffmpeg
Разбор команды:
build-essentialвключает gcc и make, необходимые для сборки программ:gcc— компилятор для языков C/C++;make— инструмент для автоматизации сборки программ;
libjpeg-dev— библиотека для обработки JPEG;libv4l-dev— библиотека для работы с Video4Linux2 (V4L2), которая используется для обработки видео с веб-камер.Cmake— это кроссплатформенная система управления сборкой, которая автоматически генерирует файлы проектов и Makefile для упрощения процесса компиляции программ;ffmpeg— инструмент для записи, конвертации, обработки и потоковой передачи аудио- и видеофайлов, поддерживающий множество форматов и кодеков.
Эти зависимости помогут правильно собрать программу для видеострима MJPG-Streamer и обрабатывать видео с веб-камеры.
Для дополнительного тестирования камеры я установлю утилиту v4l-utils:
sudo apt-get install -y v4l-utils
Утилита v4l-utils позволит мне узнать информацию о режимах камеры для её настройки и проверки.
Шаг 3: Установка MJPG-Streamer
Программа MJPG-Streamer позволяет получать видеопоток с веб-камеры, обращаясь к нему через URL-адрес. Я выбрал её, так как это простое готовое решение для видеострима.
Устанавливаю программу для видеострима:
git clone https://github.com/jacksonliam/mjpg-streamer.git cd mjpg-streamer/mjpg-streamer-experimental make && sudo make install export LD_LIBRARY_PATH=/usr/local/lib
Разбор команд:
git clonehttps://github.com/jacksonliam/mjpg-streamer.git— клонирование репозитория MJPG-Streamer;cd mjpg-streamer/mjpg-streamer-experimental— переход в директорию для сборки;make && sudo make install— компиляция и установка;export LDLIBRARYPATH=/usr/local/lib— задаёт переменную окружения для зависимостей.
Шаг 4: Проверка устройства
Теперь мне нужно понять, как обращаться к веб-камере для работы с ней. Для этого я подключаю веб-камеру к устройству через USB. Устройством может быть ПК, ноутбук или плата типа Orange pi.
Проверяю наличие подключенных видеоустройств:
ls /dev/video*
У меня два виртуальных устройства, связанных с моей веб-камерой:
/dev/video0 /dev/video1
Далее я хочу проверить, какие форматы, разрешения экрана и количество fps доступны для моей камеры.
Проверяю видеоформаты:
v4l2-ctl --list-formats-ext
Результат команды (пример):
ioctl: VIDIOC_ENUM_FMT Type: Video Capture [0]: 'MJPG' (Motion-JPEG, compressed) Size: Discrete 1280x720 Interval: Discrete 0.033s (30.000 fps) Size: Discrete 800x600 Interval: Discrete 0.033s (30.000 fps) Size: Discrete 640x480 Interval: Discrete 0.033s (30.000 fps) Size: Discrete 320x240 Interval: Discrete 0.033s (30.000 fps) [1]: 'YUYV' (YUYV 4:2:2) Size: Discrete 1280x720 Interval: Discrete 0.100s (10.000 fps) Size: Discrete 800x600 Interval: Discrete 0.050s (20.000 fps) Size: Discrete 640x480 Interval: Discrete 0.033s (30.000 fps) Size: Discrete 320x240 Interval: Discrete 0.033s (30.000 fps)
Разбор информации:
Формат MJPG (Motion-JPEG, сжатый) — обеспечивает более высокую частоту кадров (30 fps) для всех разрешений;
Формат YUYV — передаёт данные в "сыром" виде, без использования алгоритмов сжатия (видео занимает больше места) и имеет ограниченную частоту кадров для высоких разрешений;
Discrete 1280x720, Interval: Discrete 0.033s (30.000 fps) — пример разрешения экрана и интервала, который соответствует количеству кадров в секунду (указан в скобках) для данного разрешения.
Теперь, зная характеристики камеры, мне проще подобрать оптимальный режим работы для видеострима.
Проверяю получение изображения с веб-камеры:
ffplay -f v4l2 -video_size 1280x720 -i /dev/video0

Разбор команды:
-f v4l2— указывает формат захвата видео (в данном случае Video4Linux2);-video_size 1280x720— задаёт разрешение экрана (например, 1280x720);-i /dev/video0— указывает устройство, с которого будет захватываться видео.
Из информации в терминале видно, что для формата YUYV разрешение экрана соответствует 10 fps. Это подтверждается данными команды v4l2-ctl --list-formats-ext для указанного разрешения. Также появилось изображение с камеры, на котором видна моя гусеничная DIY-платформа. Изображение обновляется в реальном времени. Камера работает корректно, поэтому можно приступать к проверке видеострима.
Шаг 5: Запуск видеострима в MJPG-Streamer
Запускаю видеострим:
mjpg_streamer -i "input_uvc.so -d /dev/video0 -r 640x480 -f 15 -q 80" -o "output_http.so -p 8093 -w /usr/local/share/mjpg-streamer/www" &
Разбор команды:
nput_uvc.so— модуль для обработки UVC-видео;-d /dev/video0— указывает устройство камеры;-r 640x480 и -f 15— задают разрешение и частоту кадров;-q 80— качество JPEG;output_http.so— модуль для трансляции через HTTP;-p 8093— порт для HTTP-сервера;&— запускает процесс в фоне.
Видеострим запущен, осталось проверить его корректную работу.
Шаг 6: Проверка видеопотока
Открываю в браузере URL: http://localhost:8093/?action=stream:

На экране отображается изображение с камеры с частотой 15 fps, при которой видеопоток работает приемлемо. Для слабых устройств (например, Orange Pi Zero H2+) 15 fps достаточно. Более точные настройки для видео подберу на конкретном устройстве в ходе проверки. На мощных устройствах я могу установить до 30 fps для своей веб-камеры.
Стек технологий
Я подобрал следующий стек технологий для веб-приложения:
Python 3.12.0 — язык программирования, выбранный для разработки;
MJPG-Streamer — инструмент для стриминга видео через HTTP;
FastAPI — высокопроизводительный веб-фреймворк с поддержкой асинхронности и Websocket;
Uvicorn — лёгкий и быстрый сервер ASGI для Python, предназначенный для запуска современных веб-приложений и поддерживающий асинхронные операции;
Poetry — инструмент для управления зависимостями и сборки проектов на Python;
Pyenv — утилита для управления версиями Python, которая позволяет устанавливать, переключать и администрировать несколько версий Python на одной системе.
Перед началом написания кода я установил всё перечисленное:
Ссылка на установку Python;
Ссылка на документацию с процессом установки Poetry;
Ссылка на документацию с процессом установки Pyenv.
Виртуальное окружение и установка библиотек
Я создал виртуальное окружение следующими командами:
pyenv install 3.12.0 pyenv virtualenv 3.12.0 web_robot_control pyenv versions source ~/.bash_profile pyenv shell web_robot_control
Разбор команд:
pyenv install 3.12.0— установка версии Python 3.12.0 с использованием Pyenv;pyenv virtualenv 3.12.0 web_robot_control— создание виртуального окружения с указанной версией Python и названием;pyenv versions— отображение списка доступных виртуальных окружений и установленных версий Python;source ~/.bash_profile— активация новых настроек для использования Pyenv;pyenv shell web_robot_control— активация виртуального окружения в терминале.
Теперь у проекта есть виртуальная среда, в которую будут установлены все библиотеки и зависимости.
Устанавливаю Poetry в активированное виртуальное окружение web_robot_control:
pip install poetry
Устанавливаю фреймворк FastApi:
poetry add "fastapi[all]"
При установке fastapi[all] автоматически включаются uvicorn и другие важные библиотеки. Теперь, когда установлены все библиотеки и зависимости для них, я могу приступать к работе над самим веб-приложением.
Структура проекта
Структура проекта — это логическая организация файлов и папок внутри проекта, которая помогает упростить разработку, поддержку и расширение приложения. Poetry умеет создавать новый проект с готовой стартовой структурой, что очень удобно.
Создаю новый проект:
poetry new web-robot-control
Команда создала проект со следующей структурой:
web-robot-control ├── pyproject.toml ├── README.md ├── src │ └── web_robot_control │ └── __init__.py └── tests └── __init__.py
Разбор структуры проекта:
pyproject.toml — файл, который описывает конфигурацию проекта. В нем можно указать зависимости, инструменты для сборки и настройки (используется в
poetry,pip,setuptools);README.md — текстовый файл с описанием проекта. В нем обычно содержатся инструкции по установке, использованию и дополнительной информации;
src/ — папка с исходным кодом приложения:
web_robot_control/ — основная директория кода проекта;
init.py — файл, который делает эту директорию пакетом Python. Он может быть пустым или содержать код для инициализации пакета.
tests/ — папка с тестами для проверки работоспособности кода.
Содержимое pyproject.toml:
[project] name = "web-robot-control" version = "0.1.0" description = "Web-robot-control - open source веб-приложение для управлением роботом и трансляции видео с веб-камеры." authors = [ {name = "Arduinum628",email = "message.chaos628@gmail.com"} ] license = {text = "MIT"} readme = "README.md" requires-python = ">=3.12" dependencies = [ "fastapi[all] (>=0.115.12,<0.116.0)" ] [tool.poetry] packages = [{include = "web_robot_control", from = "src"}] [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api"
Разбор содержимого:
name = "web-robot-control" — название проекта;
version = "0.1.0" — текущая версия проекта;
description = "Web-robot-control — open source веб-приложение для управлением роботом и трансляции видео с веб-камеры." — краткое описание проекта;
authors = [{name = "Arduinum628",email = "message.chaos628@gmail.com"}] — имя автора и его электронный адрес;
license = {text = "MIT"} — тип лицензии, в данном случае MIT (открытое ПО);
readme = "README.md" — путь к README-файлу проекта;
requires-python = ">=3.12" — требуемая версия Python (не ниже 3.12);
dependencies = ["fastapi[all] (>=0.115.12,<0.116.0)" — список зависимостей, включая FastAPI;
packages = [{include = "name_project_packet", from = "src"}] — указание пакетов проекта и их пути;
requires = ["poetry-core>=2.0.0,<3.0.0"] — зависимости, необходимые для сборки проекта;
build-backend = "poetry.core.masonry.api" — механизм сборки для создания пакета.
Теперь у проекта есть настроенный конфигурационный файл. Подробнее о файле pyproject.toml можно узнать из документации Poetry.
Написание кода и документации веб-приложения
Теперь, когда настроено виртуальное окружение, установлены все зависимости и создана структура проекта, я перехожу к написанию кода и документации.
Документация
Перед началом разработки важно установить правила оформления веток и коммитов. Поскольку это open-source проект, другие разработчики, которые захотят дополнить приложение, должны иметь чёткие рекомендации по работе с ветками и коммитами, а также описание проекта. Это помогает организовать процесс разработки, избежать хаоса и понимать разработчикам и пользователям для чего приложение написано. Документация хранится в файле README.md, который уже был ранее создан командой poetry new web-robot-control. Осталось его наполнить содержимым.
Содержимое README.md:
# Web-robot-control **Web-robot-control** - open source веб-приложение для управлением роботом и трансляции видео с веб-камеры. ## Запуск приложения **Запуск для локальной разработки (бекенд)**: `poetry run uvicorn web_robot_control.main:app --host server_ip --port port_number` **Todo:** создать Python-функцию для запуска веб-приложения и добавить её в скрипты Poetry <details> <summary> <strong> Как оформлять ветки и коммиты </strong> </summary> Пример ветки `user_name/name_task` - **user_name** (имя пользователя); - **name_task** (название задачи). Пример коммита `refactor: renaming a variable` - **feat:** (новая функционал кода, БЕЗ учёта функционала для сборок); - **devops:** (функционал для сборки, - добавление, удаление и исправление); - **fix:** (исправление ошибок функционального кода); - **docs:** (изменения в документации); - **style:** (форматирование, отсутствующие точки с запятой и т.п., без изменения производственного кода); - **refactor:** (рефакторинг производственного кода, например, переименование переменной); - **test:** (добавление недостающих тестов, рефакторинг тестов; без изменения производственного кода); - **chore:** (обновление рутинных задач и т. д.; без изменения производственного кода). Оформление основано на https://www.conventionalcommits.org/en/v1.0.0/ </details>
Правила оформления веток и коммитов основаны на спецификации Conventional Commits. Это помогает структурировать изменения, чтобы они были понятными, единообразными и легко воспринимаемыми.
Кроме того, я добавил описание проекта, чтобы ясно объяснить, что это за приложение и для каких целей оно создано. Также включён пример команды для локального запуска приложения:
poetry run uvicorn web_robot_control.main:app --host server_ip --port port_number
.env.example
Я создал файл .env.example, который служит для описания переменных окружения.
STREAM_URL="url-адрес видеопотока"
В реальном .env файле я добавлю url-адрес видеопотока:
STREAM_URL=http://localhost:8093/?action=stream
settings.py
Я создал файл settings.py для хранения настроек проекта, прочитанных из файла .env, и валидации типов переменных окружения. Для этих целей используется библиотека pydantic_settings. Она предоставляет готовые классы для валидации, чтения .env файла и других задач.
Содержимое settings.py:
from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """Класс для данных конфига""" model_config = SettingsConfigDict( env_file = '.env', env_file_encoding='utf-8', extra='ignore' ) stream_url: str settings = Settings()
Пояснения к коду settings.py:
class Settings(BaseSettings)— класс, который сохраняет переменные окружения, загруженные из .env файла, для настройки проекта;model_config = SettingsConfigDict(...)— переменная для задания конфигурации модели:env_file='.env'— указывает, что настройки загружаются из файла .env;env_file_encoding='utf-8'— задаёт кодировку файла .env;extra='ignore'— игнорирует переменные, которые не описаны в классе;stream_url: str— переменная для хранения URL-видеострима;settings = Settings()— создание экземпляра класса с настройками.
views.py
Теперь я приступаю к созданию самого веб-приложения, начиная с файла views.py. Этот файл обрабатывает запросы и отправляет ответы клиенту.
FastAPI поддерживает WebSocket, который я использую для получения команд с фронтенда в реальном времени. Это решение не мешает видеостримингу. Более подробно ознакомиться с WebSocket можно узнать на странице документации.
Содержимое views.py:
from fastapi import APIRouter, WebSocket from fastapi.requests import Request from fastapi.responses import HTMLResponse, Response from fastapi.templating import Jinja2Templates from starlette.websockets import WebSocketDisconnect import httpx from web_robot_control.settings import settings # Создаем объект роутера router = APIRouter() # Создаём объект для рендеринга html-шаблонов templates = Jinja2Templates(directory='static') @router.get('/', response_class=HTMLResponse) async def index(request: Request) -> Response: """ Асинхронная функция для получения главной страницы приложения. """ return templates.TemplateResponse( request=request, name='index.html', context={'title': 'Web-robot-control - Главная', 'name_robot': 'Bot1'} ) @router.get('/config') async def get_config() -> dict: """Aсинхронная функция для получения stream_url.""" return {'stream_url': settings.stream_url} @router.websocket('/ws') async def websocket_endpoint(websocket: WebSocket) -> None: # Установка содединения по веб-сокету await websocket.accept() try: async with httpx.AsyncClient() as client: while True: # Получение команды от клиента (с веб-сокета) command = await websocket.receive_text() print(f'Получена команда: {command}') # Todo: здесь будет логика валидации команд # Todo: здесь будет логика обработки команды except WebSocketDisconnect: print('WebSocket отключен') # Todo: для вывода ошибок будет настроен logger # Todo: вместо Exception будут добавлена ловля других ошибок # (после того как функция будет полностью дописана) except Exception as err: err_text = f'Ошибка: {str(err)}' await websocket.send_text(err_text) print(err_text)
Импорты и инициализация объектов:
from fastapi import APIRouter, WebSocket— модули для работы с маршрутами HTTP и WebSocket;from fastapi.responses import HTMLResponse— класс для возврата HTML-контента в HTTP-ответе;from starlette.websockets import WebSocketDisconnect— исключение для обработки отключения WebSocket соединения;import httpx— библиотека для выполнения HTTP-запросов асинхронно;from web_robot_control.settings import settings— настройки приложения (например, переменные из .env);router = APIRouter()— объект роутера;templates = Jinja2Templates(directory='static')— объект для рендеринга HTML-шаблонов.
index() — функция для отображения главной страницы:
@router.get('/', response_class=HTMLResponse)— связывает функцию с URL '/' для GET-запроса и задаёт тип ответа HTMLResponse;return templates.TemplateResponse(...)— возвращает HTML-шаблон с контекстом:request=request — передаёт запрос в шаблонизатор;
name='index.html' — указывает имя HTML-шаблона;
context={'title': ..., 'name_robot': ...} — передаёт данные для использования в шаблоне.
get_config() — функция для получения настроек:
@router.get('/config')— связывает функцию с URL /config для GET-запроса;return {'stream_url':settings.stream_url}— возвращает словарь с URL видеострима в виде JSON-ответа.
websocket_endpoint() — функция для обработки команд через WebSocket:
@router.websocket('/ws')— связывает функцию с маршрутом /ws для обработки WebSocket-запросов;await websocket.accept()— устанавливает соединение между сервером и клиентом;async with httpx.AsyncClient() as client:— создаёт асинхронного HTTP-клиента (пока заготовка);command = await websocket.receive_text()— получает команду от клиента через WebSocket;print(f'Получена команда: {command}')— вывод команды в консоль (для тестирования);except WebSocketDisconnect:— ловит разрыв соединения и выводит сообщение;except Exception as err:— ловит общие ошибки и отправляет их клиенту через WebSocket.
Теперь у бекенда есть возможность получать команды от клиента (фронтенда), передавать данные конфига на фронтенд через WebSocket и получать html главной страницы.
main.py
Файл main.py — это главный файл приложения, который отвечает за его создание и запуск. В этом файле подключаются маршруты, статические файлы и другие компоненты приложения.
Содержимое main.py:
from fastapi import FastAPI from starlette.staticfiles import StaticFiles from web_robot_control.views import router # создаем экземпляр FastAPI app = FastAPI() # подключаем статические файлы app.mount('/static', StaticFiles(directory='static'), name='static') # подключаем роутер app.include_router(router)
Импорты:
from fastapi import FastAPI— импорт класса FastAPI, который используется для создания основного приложения;from starlette.staticfiles import StaticFiles— импорт класса StaticFiles, который отвечает за обслуживание статических файлов (например, CSS, JavaScript, изображения);from web_robot_control.views import router— импорт маршрутизатора router, в котором определены обработчики запросов (созданные ранее в views.py).
Приложение в FastApi:
app = FastAPI()— создание экземпляра основного приложения;app.mount('/static', StaticFiles(directory='static'), name='static'):'/static' — URL, по которому будут доступны статические файлы;
StaticFiles(directory='static') — указывает путь к папке с статическими файлами;
name='static' — имя маршрута, позволяющее ссылаться на него в других частях приложения;
app.include_router(router)— подключение маршрутизатора, который добавляет маршруты из views.py в приложение.
Бекендная часть готова, теперь осталось написать фронтенд для веб-приложения.
index.html
Я создаю простой фронтенд, который бу��ет выполнять роль клиента. Сначала я разработал файл index.html, предназначенный для отображения кнопок управления и видеострима. Я буду использовать Bootstrap для ускорения и упрощения разработки фронтенда. Bootstrap — это популярный фреймворк для создания адаптивных веб-интерфейсов, который включает готовые стили, компоненты и JavaScript-инструменты для ускорения разработки.
Содержимое index.html:
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{ title }}</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet"> <link rel="stylesheet" href="/static/style.css"> <script src="/static/command.js"></script> </head> <body> <div class="container mt-5"> <h1 class="text-center mb-4">Управление роботом {{ name_robot }}</h1> <!-- Видеопоток --> <div class="row justify-content-center"> <div class="col-md-7 px-0"> <div class="card"> <div class="card-body text-center"> <img src="" class="img-fluid" id="video-stream" alt="Видеопоток"> </div> <div class="line"></div> <!-- Кнопки --> <div class="col-md-12 d-flex align-items-center px-0"> <div class="card-command card d-flex flex-column justify-content-center align-items-center"> <!-- Вверх --> <button class="btn btn-warning m-1" id="forward-button"> <i class="bi bi-arrow-up"></i> </button> <div class="d-flex"> <!-- Влево --> <button class="btn btn-warning m-1" id="left-button"> <i class="bi bi-arrow-left"></i> </button> <!-- Вниз --> <button class="btn btn-warning m-1" id="backward-button"> <i class="bi bi-arrow-down"></i> </button> <!-- Вправо --> <button class="btn btn-warning m-1" id="right-button"> <i class="bi bi-arrow-right"></i> </button> </div> </div> </div> </div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> </body> </html>
Главные особенности index.html:
<title>{{ title }}</title>— шаблонное выражение для динамического указания заголовка страницы (его передали в context на бэкенде);href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">— подключение CSS-файла из CDN Bootstrap для применения стилей для приложения;<link rel="stylesheet" href="/static/style.css">— подключение локального CSS-файла (style.css), где находятся пользовательские стили;<script src="/static/command.js"></script>— подключение локального JavaScript-файла (command.js), который управляет командами (например, отправка команд WebSocket'у).<h1 class="text-center mb-4">Управление роботом {{ name_robot }}</h1>— Шаблонное выражение для вывода имени робота и красивое оформление заголовка (выравнивание по центру).<img src="" class="img-fluid" id="video-stream" alt="Видеопоток">— Элемент для отображения видеопотока от робота, где src будет динамически обновляться через JavaScript;<!-- Кнопки -->— cекция с кнопками управления роботом (вперёд, назад, влево, вправо).
style.css
Я добавил пользовательские стили, чтобы слегка кастомизировать стандартные стили Bootstrap.
Содержимое style.css:
/* Задает общий фон страницы / body { background-color: #f8f9fa; } / Создает тень для карточки, добавляя глубину и визуальную привлекательность / .card { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } / Устанавливает черный цвет фона для элементов с классами card-command и card-body / .card-command, .card-body { background-color: black; } / Устанавливает стили для блока card-command / .card-command { width: 100%; / Задает ширину в 100% / border-top-left-radius: 0; / Убирает закругление верхнего левого угла / border-top-right-radius: 0; / Убирает закругление верхнего правого угла / } / Определяет стили для горизонтальной линии между элементами / .line { background-color: #ffc107; / Задает желтый цвет линии / height: 6px; / Высота линии / width: 100%; / Линия занимает всю ширину блока / } / Определяет максимальную ширину изображения и делает его адаптивным / img { max-width: 100%; / Ограничивает ширину изображения, чтобы оно не выходило за пределы контейнера / height: auto; / Автоматически изменяет высоту изображения пропорционально ширине */ }
command.js
Теперь, когда файлы index.html и style.css для главной страницы готовы, я добавил логику для работы клиента. Для этого я создал файл command.js, который отвечает за обновление видеострима, вывод сообщений в консоль для отладки, а также управление WebSocket и передачу команд бекенду.
Содержимое command.js:
// Ждём, пока загрузится весь контент DOM document.addEventListener("DOMContentLoaded", () => { // Создаём WebSocket-соединение с сервером const ws = new WebSocket(ws://${window.location.host}/ws); // Делаем запрос к серверу на эндпоинт "/config" fetch('/config') .then(response => response.json()) // Преобразуем ответ в JSON .then(data => { // Получаем значение stream_url из ответа const streamUrl = data.stream_url; const videoElement = document.getElementById("video-stream"); // Устанавливаем URL видеопотока в элемент <video> videoElement.src = streamUrl; }); // Обработчик события открытия WebSocket-соединения ws.onopen = function() { console.log("WebSocket подключен"); }; // Обработчик события получения сообщения по WebSocket ws.onmessage = function(event) { console.log("Получено:", event.data); // Выводим полученные данные }; // Обработчик события закрытия WebSocket-соединения ws.onclose = function() { console.log("WebSocket закрыт"); }; // Обработчик ошибок WebSocket ws. { console.log("WebSocket ошибка:", error); // Логируем ошибку }; let commandInterval; // Функция для начала отправки команды с заданным интервалом function startSendingCommand(command) { // Отправляем команду сразу sendCommand(command); // Запускаем интервал для повторной отправки команды commandInterval = setInterval(() => { sendCommand(command); // Повторяем отправку команды }, 10); // Интервал отправки — каждые 10 мс } // Функция для остановки отправки команд function stopSendingCommand() { // Останавливаем интервал clearInterval(commandInterval); } // Функция для отправки команды через WebSocket function sendCommand(command) { if (ws.readyState === WebSocket.OPEN) { ws.send(command); // Отправляем команду через WebSocket console.log("Команда:", command); // Логируем отправленную команду } else { console.log("WebSocket не подключён"); // Если WebSocket не открыт } } // Назначение обработчиков событий для кнопки "Вперёд" const forwardButton = document.getElementById("forward-button"); forwardButton.addEventListener("mousedown", () => startSendingCommand("forward")); // Начало отправки команды forwardButton.addEventListener("mouseup", stopSendingCommand); // Остановка отправки при отпускании кнопки forwardButton.addEventListener("mouseleave", stopSendingCommand); // Остановка отправки, если курсор уходит с кнопки // Назначение обработчиков событий для кнопки "Влево" const leftButton = document.getElementById("left-button"); leftButton.addEventListener("mousedown", () => startSendingCommand("left")); leftButton.addEventListener("mouseup", stopSendingCommand); leftButton.addEventListener("mouseleave", stopSendingCommand); // Назначение обработчиков событий для кнопки "Вправо" const rightButton = document.getElementById("right-button"); rightButton.addEventListener("mousedown", () => startSendingCommand("right")); rightButton.addEventListener("mouseup", stopSendingCommand); rightButton.addEventListener("mouseleave", stopSendingCommand); // Назначение обработчиков событий для кнопки "Назад" const backwardButton = document.getElementById("backward-button"); backwardButton.addEventListener("mousedown", () => startSendingCommand("backward")); backwardButton.addEventListener("mouseup", stopSendingCommand); backwardButton.addEventListener("mouseleave", stopSendingCommand); });
Особенность моего управления в том, что команда отправляется каждые 10 мс, пока пользователь удерживает кнопку. Задержку уточним экспериментально на реальном роботе. Отправка команд прекращается, как только кнопка отпускается.
Ссылка на получившийся в итоге open-source проект web-robot-control.
Итоговая cтруктура проекта
Структура теперь имеет следующий вид:
web-robot-control ├── src │ └── web_robot_control │ ├── __pycache__ │ ├── __init__.py │ ├── main.py │ ├── settings.py │ ├── views.py ├── static │ └── command.js │ └── index.html │ └── style.css ├── tests │ └── __init__.py ├── .env ├── .env.example ├── .gitignore ├── LICENSE ├── poetry.lock ├── pyproject.toml └── README.md
Структура проекта соответствует принципу разделению ответственности файлов. Одна из его функций — определять, где должен находится каждый файл, чтобы код оставался упорядоченным, удобным для поддержки и расширения.
Web-приложение в действии
Запускаю веб-приложение:
poetry run uvicorn web_robot_control.main:app --host localhost --port 8095
Разбор команды:
poetry run— запускает команду внутри виртуальной среды, созданной Poetry;uvicorn web_robot_control.main:app— запускает ASGI-сервер Uvicorn, указывая:web_robot_control.main— модуль Python, где находится приложение;app— экземпляр приложения FastAPI, который будет запущен.
--hostlocalhost— сервер будет слушать локальный хост, то есть доступен только на текущем устройстве;--port 8095— приложение будет запущено на порту 8095.
Работа веб-приложения в браузере по URL http://localhost:8095:
На видео видно, что видеострим успешно работает в реальном времени, а команды "left", "right", "forward", "backward" отображаются в консоли браузера. Я управляю роботом обычным ИК-пультом.
Работа бэкенда:
INFO: Started server process [43146] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://localhost:8095 (Press CTRL+C to quit) INFO: ::1:37202 - "GET / HTTP/1.1" 200 OK INFO: ::1:37202 - "GET /static/command.js HTTP/1.1" 304 Not Modified INFO: ('::1', 47800) - "WebSocket /ws" [accepted] INFO: ::1:37202 - "GET /config HTTP/1.1" 200 OK INFO: connection open Получена команда: forward Получена команда: right Получена команда: backward Получена команда: left ^CINFO: Shutting down WebSocket отключен INFO: connection closed INFO: Waiting for application shutdown. INFO: Application shutdown complete. INFO: Finished server process [17634]
Бэкенд успешно принимает команды и выводит результат в терминал:
Получена команда: forward;
Получена команда: right;
Получена команда: backward;
Получена команда: left.
Заключение и планы на будущее
Были установлены все необходимые зависимости и библиотеки для видеострима. Видеострим с веб-камеры успешно протестирован. Настроено виртуальное окружение, а также установлены зависимости для проекта. Написан код, обеспечивающий отправку команд на сервер и отображение видеострима. В результате получилась достойная основа для open-source проекта, которая пока ещё не достигла статуса MVP1 и требует доработки.
Хочу добавить в проект logger — инструмент для ведения логов, который будет помогать отслеживать события во время работы программы. Также планирую реализовать валидацию команд и механизм их отправки роботу. Кроме того, нужно добавить утилиту для робота, которая будет принимать команды с сервера. Без неё робот не получит команды управления. Если у вас есть идеи по развитию проекта, поделитесь ими в комментариях — буду рад услышать ваши предложения!
Автор статьи @Arduinum
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
— 15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.
