Рестриминг видеопотока ESP32 JPEG

Я создаю домашнюю систему наблюдения своими руками из 30 камер ESP32 AI Thinker. Чтобы присоединиться ко всем и управлять ими, я создаю веб-приложение NodeJS — панель управления для просмотра всех камер, добавления/редактирования/удаления их. Каждая камера представляет собой чип ESP32, который передает потоковое видео в формате JPEG (кадры jpeg один раз в X мс). Каждая камера имеет свой собственный IP-адрес в домашней сети, а это означает, что у каждой камеры есть простой веб-сервер для просмотра видео. Но поскольку это ESP32 и простой веб-сервер, к веб-серверу не может быть двух или более подключений. Допустим, у меня есть вкладка браузера, подключенная к стримеру, и когда я дублирую эту вкладку, загрузка занимает бесконечное время, и сразу после закрытия первой вкладки - вторая вкладка показывает видео. Панель инструментов камер должна иметь возможность отображать несколько подключений, но, столкнувшись с проблемой веб-соединения ESP32, я решил сделать что-то вроде повторного стримера на Python или nodejs, который будет «читать» со всех камер (поэтому каждая камера будет иметь 1 соединение) и повторно выполнить потоковую передачу на несколько соединений.

Опять же, поток камер ESP32, повторный стример считывает каждую камеру и выполняет повторную потоковую передачу, панель мониторинга считывает данные из повторного стримера.

Идеальный дизайн – это:

Дашборд готов — это обычный CRUD с дополнительным сервисом — смотрите камеру

ESP32 готов.

Осталось написать этот рестример и я потратил около недели на поиск решения. Проблема в том, что ничто из того, что я пробовал, не может прочитать поток JPEG. Да, это поток JPEG, а не, скажем, протокол RTSP или HSP. Когда я сворачиваю IP-адрес камеры, я вижу тип контента image/jpeg.

Может ли кто-нибудь сказать мне, как прочитать и повторно перевести поток JPEG, чтобы я мог показать его на панели инструментов? FFmpeg не помог

нет документации по формату потока? можешь опубликовать, что произойдет, если ты это curl --raw?

mb21 22.05.2024 16:48

Формат потока — просто image/jpeg. А когда вы запрашиваете камеру по ее IP, на самом деле ответ так и не завершился. Идет бесконечная загрузка с потоковой передачей изображений в формате jpeg... Вот и результат. alexey@alexey-ASUS:~$ curl --raw 192.168.2.160 33 Тип контента: image/jpeg Длина контента: 17452 442c Внимание: двоичный вывод может испортить ваш терминал. Используйте «--output -», чтобы указать Предупреждение: Curl все равно выводить его на терминал, или рассмотрите «--output Предупреждение: <FILE>», чтобы сохранить в файл.

Alexey Khachatryan 22.05.2024 19:07

Точно. Но есть проблема — чтобы показать видео в теге <img> — нужно отправить запрос на сервер. В моем случае сервером является ESP32, который может параллельно обрабатывать только одно соединение. А если я открою свой дашборд на 3 ПК, то только один сможет показывать видео, а 2 будут грузиться бесконечно

Alexey Khachatryan 22.05.2024 20:53
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
4
177
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Итак, через месяц я наконец нашел решение. Результат здесь: https://github.com/Electr0Hub/HomeSauron-Aggregator

Сделал следующим образом: Есть команда, которая представляет собой процесс (разновидность ремесленных команд laravel). Эта команда просыпается, получает все камеры из базы данных, проходит по всем камерам с помощью цикла и вызывает другую команду для каждой камеры, т. е. запускает независимые команды и передает идентификатор камеры этой команде. Дочерний (который независим) получает URL-адрес камеры из БД, используя свой идентификатор, выполняет бесконечное чтение и записывает фрагменты в публикацию/подписку Redis. Итак, в целом у меня есть одна «родительская» команда, которая запускает все «дочерние» команды для пробуждения, чтения и записи в Redis. Очень просто.

Я сделал это в Laravel с его командами Artisan. Так что я:

рестриминг PHP Artisan: камеры

public function handle()
    {
        // Kill all previous instances of the `restream:camera` command
        $this->killPreviousProcesses();

        // Retrieve all cameras
        $cameras = Camera::select('id')->get();

        foreach ($cameras as $camera) {
            $cameraId = $camera->id;
            $this->info("Starting restream for camera ID: $cameraId");

            // Execute the restream:camera command in the background
            exec("php artisan restream:camera --camera = {$cameraId} > /dev/null &");
        }
    }

    /**
     * Kill all previous instances of the `restream:camera` command.
     */
    protected function killPreviousProcesses(): void
    {
        // Get all the PIDs of `restream:camera` processes
        exec("ps aux | grep 'php artisan restream:camera ' | grep -v grep | awk '{print $2}'", $output);

        foreach ($output as $pid) {
            // Kill each process
            exec("kill -9 $pid");
        }
    }

И дочерний — php artisan restream:camera --camera = {CAMERAID}

public function handle()
    {
        try {
            // Retrieve the camera option
            $cameraOption = $this->option('camera');

            // Check if the camera option is provided
            if (!$cameraOption) {
                $this->error('The --camera option is required.');
                return;
            }

            // Fetch the camera(s) based on the option
            $camera = Camera::find($cameraOption);

            if (is_null($camera)) {
                $this->error('No cameras found with the provided option.');
                return;
            }

            $client = new Client();

            $this->restreamCamera($client, $camera);
        }
        catch (\Exception $exception) {
            Log::channel("streams")->error($exception);
            throw $exception;
        }
    }

    protected function restreamCamera(Client $client, Camera $camera): void
    {
        $url = $camera->url;

        $client->getAsync($url, [
            'stream' => true,
            'sink' => fopen('php://temp', 'r+')
        ])->then(
            function (Response $response) use ($camera) {
                $this->info('Streaming frame from ' . $camera->id);
                $body = $response->getBody();
                $frameBuffer = '';

                while (!$body->eof()) {
                    // Read a chunk of data
                    $frame = $body->read(1024); // Adjust the buffer size as needed
                    $frameBuffer .= $frame;

                    // Check if the buffer contains more than one boundary
                    if (substr_count($frameBuffer, '123456789000000000000987654321') > 1) {
                        // Extract data between boundaries
                        $jpegData = $this->getDataBetweenBoundary($frameBuffer, '123456789000000000000987654321', '123456789000000000000987654321');

                        // Find the JPEG start marker
                        $jpegStart = strpos($jpegData, "\xFF\xD8");
                        if ($jpegStart !== false) {
                            // Extract the JPEG data from the start marker
                            $jpegData = substr($jpegData, $jpegStart);
                            // Publish the JPEG data to Redis

                            $dataToPublish = [
                                'frame' => base64_encode($jpegData),
                                'camera' => [
                                    'id' => $camera->id,
                                    'name' => $camera->name,
                                    'url' => $camera->url,
                                ],
                            ];
                            Redis::publish("camera_stream:$camera->id", json_encode($dataToPublish));

                            // Reset the buffer to remove processed data
                            $frameBuffer = substr($frameBuffer, strpos($frameBuffer, '123456789000000000000987654321', strpos($frameBuffer, '123456789000000000000987654321') + 1));
                        }
                    }
                }
            },
            function ($error) {
                // Handle the error
                echo "Error: " . $error->getMessage();
            }
        );
    }

    protected function getDataBetweenBoundary($string, $start, $end){
        $string = ' ' . $string;
        $ini = strpos($string, $start);
        if ($ini == 0) return '';
        $ini += strlen($start);
        $len = strpos($string, $end, $ini) - $ini;
        return substr($string, $ini, $len);
    }

Другие вопросы по теме