Сервер видеостриминга WebRTC (Python) застрял на получении кадров

Вопрос или проблема

Предыстория:

Я работаю над сервером видеостриминга WebRTC с использованием aiortc и aiohttp на Python. Цель состоит в том, чтобы сервер захватывал кадры с IP-камеры, обрабатывал их и затем стримил на клиент. Для тестирования это упрощённая версия кода, в которой кадры захватываются с IP-камеры и непосредственно стримятся на клиент без дополнительной обработки (в оригинальной версии кадры обрабатываются перед отправкой клиенту).

Проблема:

Несмотря на успешную переговора ICE и получение дорожки на стороне клиента, клиент зависает при попытке получить видеокадры, и кажется, что метод recv() сервера (который захватывает и отправляет кадры) никогда не срабатывает.

Я объясню настройку и поделюсь соответствующим кодом как для сервера (stream.py), так и для клиента (client.py).

Сервер: stream.py

import cv2
import asyncio
import logging
import time
import numpy as np
from aiortc import VideoStreamTrack, RTCPeerConnection, RTCSessionDescription
from aiortc.mediastreams import VideoFrame
from aiohttp import web

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("WebRTC")

# WebRTC дорожка для видеостриминга
class VideoProcessorTrack(VideoStreamTrack):
    def __init__(self, camera_url, desired_fps=15):
        super().__init__()
        self.camera_url = camera_url
        self.cap = cv2.VideoCapture(self.camera_url)
        self.desired_fps = desired_fps
        self.frame_interval = 1.0 / self.desired_fps  # Временной интервал между кадрами

        if not self.cap.isOpened():
            logger.error(f"Не удалось открыть поток камеры: {self.camera_url}")
        else:
            logger.info(f"Поток камеры успешно открыт: {self.camera_url}")

    async def recv(self):
        """Захват и обработка кадров с камеры."""
        logger.info("Вход в метод recv() для захвата кадра")
        await asyncio.sleep(self.frame_interval)

        # Захват кадра
        ret, frame = self.cap.read()
        if not ret:
            logger.warning("Не удалось считать кадр, отправляем пустой кадр...")
            frame = np.zeros((480, 640, 3), dtype=np.uint8)  # Заглушка черный кадр
        else:
            logger.info("Успешно захвачен кадр с камеры")

        # Конвертация кадра для WebRTC
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        video_frame = VideoFrame.from_ndarray(frame_rgb, format="rgb24")
        video_frame.pts = time.time()
        video_frame.time_base = 1 / self.desired_fps
        logger.info("Кадр готов к отправке")
        return video_frame

async def offer(request):
    """Обработать входящее предложение WebRTC и отправить ответ."""
    try:
        logger.info("Получен запрос предложения от клиента")
        params = await request.json()
        logger.info(f"Разобрано предложение: {params}")

        offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
        pc = RTCPeerConnection()

        async def on_iceconnectionstatechange():
            logger.info(f"Состояние ICE соединения: {pc.iceConnectionState}")
            if pc.iceConnectionState == "completed":
                logger.info("ICE соединение успешно завершено!")
            elif pc.iceConnectionState == "failed":
                logger.error("ICE соединение провалено.")

        pc.on("iceconnectionstatechange", on_iceconnectionstatechange)

        # Добавить видеодорожку в соединение
        player = VideoProcessorTrack(camera_url="http://129.125.136.20/axis-cgi/mjpg/video.cgi?camera=1", desired_fps=15)
        logger.info("Добавление видеодорожки в соединение")
        pc.addTrack(player)

        # Логировать каждый раз, когда мы получаем запрос на кадр
        @pc.on("track")
        async def on_track(track):
            logger.info(f"Дорожка {track.kind} получена на сервере")

        # Обработка обмена предложения/ответа WebRTC
        await pc.setRemoteDescription(offer)
        answer = await pc.createAnswer()
        await pc.setLocalDescription(answer)

        logger.info("Локальное описание установлено с ответом")
        return web.json_response({"sdp": pc.localDescription.sdp, "type": pc.localDescription.type})
    
    except Exception as e:
        logger.error(f"Ошибка в обработке предложения: {e}")
        return web.Response(text="Внутренняя ошибка сервера", status=500)

app = web.Application()
app.router.add_post("/offer", offer)

if __name__ == "__main__":
    logger.info("Запуск сервера видеостриминга WebRTC...")
    web.run_app(app, port=8081)

Клиент: client.py

import asyncio
import cv2
from aiortc import RTCPeerConnection, RTCSessionDescription, MediaStreamTrack
import aiohttp
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("WebRTC Client")

class DummyVideoTrack(MediaStreamTrack):
    kind = "video"

    async def recv(self):
        return None

async def run_client():
    pc = RTCPeerConnection()

    dummy_track = DummyVideoTrack()
    pc.addTrack(dummy_track)

    @pc.on("track")
    async def on_track(track):
        logger.info(f"Получение дорожки: {track.kind}")

        if track.kind == "video":
            cv2.namedWindow("WebRTC Stream", cv2.WINDOW_NORMAL)

            while True:
                try:
                    frame = await track.recv()  # Получение кадра из дорожки
                    img = frame.to_ndarray(format="bgr24")  # Конвертация кадра в массив numpy
                    logger.info(f"Кадр получен: {img.shape}")

                    # Отображение кадра с помощью OpenCV
                    cv2.imshow("WebRTC Stream", img)

                    if cv2.waitKey(1) & 0xFF == ord('q'):
                        break  # Выход при нажатии 'q'
                except Exception as e:
                    logger.error(f"Ошибка при обработке кадра: {e}")
                    break  # Прерывание цикла в случае ошибок

            cv2.destroyAllWindows()  # Закрытие окна по завершении
            track.stop()

    # Создание предложения WebRTC
    offer = await pc.createOffer()
    await pc.setLocalDescription(offer)

    # Отправка предложения на сервер
    offer_payload = {
        "sdp": pc.localDescription.sdp,
        "type": pc.localDescription.type,
    }
    logger.info("Отправка предложения на сервер")

    async with aiohttp.ClientSession() as session:
        async with session.post("http://localhost:8081/offer", json=offer_payload) as response:
            if response.status != 200:
                logger.error(f"Ошибка сервера: {response.status}")
                return
            answer = await response.json()
            logger.info("Получен ответ от сервера")

        await pc.setRemoteDescription(RTCSessionDescription(sdp=answer["sdp"], type=answer["type"]))
        logger.info("Удаленное описание установлено на клиенте")

    # Поддержание соединения открытым
    await asyncio.Future()

if __name__ == "__main__":
    asyncio.run(run_client())

Что работает:

  • Клиент успешно отправляет предложение на сервер, а сервер отвечает ответом.
  • Переговоры ICE завершаются успешно, и находится валидная пара кандидатов.
  • Клиент логирует получение дорожки: видео, указывая, что сервер отправляет видеодорожку.

Проблема:

Несмотря на успешное соединение ICE и обмен медиа-дорожками, клиент зависает на:

frame = await track.recv()

Кажется, что метод recv() в VideoProcessorTrack сервера (который захватывает и отправляет кадры) никогда не срабатывает. Это означает, что клиент ждет кадры, которые никогда не отправляются.

С чем мне нужна помощь:

  • Почему метод recv() на сервере не срабатывает?
  • Есть ли что-то неправильное в переговорах дорожки WebRTC между клиентом и сервером, что мешает серверу отправлять кадры?
  • Как я могу правильно запустить сервер, чтобы он начал отправлять кадры клиенту?

Любая помощь или советы будут очень полезны!

Ответ или решение

Проблема с WebRTC Video Streaming Server на Python: Зацикливание на Получении Кадров

Введение

Вы работаете над сервером видеостриминга с использованием WebRTC, и ваша цель заключается в захвате кадров с IP-камеры, их обработке и передаче клиенту. При этом вы столкнулись с проблемой: несмотря на успешное завершение ICE-торга и получение треков на стороне клиента, клиент не может получить видеокадры, поскольку метод recv() на сервере, вероятно, не вызывается.

В этой статье мы подробно рассмотрим структуру вашего кода, выявим потенциальные причины проблемы и предложим пути ее решения.

Описание проблемы

Сервер, основанный на ваших кодах stream.py и client.py, успешно обрабатывает сигнализацию WebRTC, однако клиент застревает в ожидании:

frame = await track.recv()

Это указывает на то, что метод recv() в классе VideoProcessorTrack не вызывается, и клиент ожидает кадры, которые так и не были отправлены.

Анализ кода сервера

Давайте проанализируем ключевые моменты в вашем серверном коде:

  1. Инициализация трека: Ваша реализация VideoProcessorTrack выглядит корректно. Вы открываете поток камеры и устанавливаете нужные параметры для частоты кадров, что также выглядит разумно.

  2. Метод recv(): Этот метод должен захватывать кадры и отправлять их клиенту. Обратите внимание на оператор await asyncio.sleep(self.frame_interval) — он заставляет сервер ждать перед захватом следующего кадра, что в целом нормально. Однако, если задержка из-за обработки кадров слишком велика или возникает блокировка, это может привести к застреванию.

  3. Логирование: У вас есть логирование, что хорошо. Однако, убедитесь, что сообщения об успешном захвате кадров и отправке их клиентам действительно печатаются.

Потенциальные причины проблемы

  1. Проблемы с захватом кадров: Убедитесь, что ваша IP-камера действительно доступна и передает данные. Используйте простой скрипт OpenCV для проверки:

    import cv2
    
    cap = cv2.VideoCapture("http://129.125.136.20/axis-cgi/mjpg/video.cgi?camera=1")
    
    while True:
        ret, frame = cap.read()
        if not ret:
            print("Не удалось захватить кадр")
            break
        cv2.imshow("Frame", frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
    cap.release()
    cv2.destroyAllWindows()
  2. Отсутствие вызова метода recv(): Если не происходит вызов метода recv() из-за неправильного состояния WebRTC-соединения или событий ICE, это также может стать причиной проблемы. Убедитесь, что вы правильно создали и добавили трек к объекту RTCPeerConnection.

  3. Задержка в I/O операциях: Возможно, есть блокировки в вашем коде, которые мешают выполнению recv(). Если у вас есть другие задачи в asyncio, попробуйте их отключить и протестировать только видеопоток.

Предложения по решению

  1. Отладка метода recv(): Добавьте больше логов внутри метода recv(), чтобы видеть, вызывается ли он вообще на сервере. Если он не вызывается, возможно, стоит проверить создание и регистрацию трека.

  2. Асинхронность: Убедитесь, что функции не блокируют основной поток. Используйте asyncio для управления асинхронностью должным образом.

  3. Упрощение формы: Для тестирования можно временно упростить серверный код, исключив часть логики и оставить только базовый опыт с recv(), чтобы проверить, действительно ли проблема кроется в самом WebRTC или в других частях кода.

  4. Проверка на стороне клиента: Убедитесь, что клиент будет готов получать кадры. Это подразумевает, что клиент присоединился к серверу и установил все необходимые соединения.

Заключение

Работа с WebRTC может быть сложной из-за асинхронности и неопределенности сетевых состояний. Однако, следуя указанным шагам и предложенным решениям, вы сможете выявить и устранить проблему, из-за которой ваш видео стриминг застревает на стадии получения кадров. Будьте внимательны к логам и экспериментируйте с кодом, чтобы найти оптимальное решение. Надеюсь, что эти советы помогут вам успешно завершить вашу реализацию видеостриминга.

Оцените материал
Добавить комментарий

Капча загружается...