Перейти к основному содержимому

07-06-04 Получение кадров в приложение: appsink как мост в код

Одно из главных преимуществ GStreamer перед другими мультимедийными инструментами — его способность быть не просто утилитой для вывода видео в окно, а интегрированной частью программного приложения. В этой части мы покажем, как с помощью элемента appsink «достать» видеоданные из пайплайна и использовать их в собственном коде — например, для анализа с помощью OpenCV, передачи в виртуальную камеру или интеграции с OBS через плагин.

Этот подход лежит в основе многих современных медиа-приложений: от систем видеонаблюдения с детекцией объектов до стриминговых решений с динамическими оверлеями.


Что такое appsink?

appsink — это специальный приёмный элемент (sink) в GStreamer, который не выводит видео на экран и не сохраняет его в файл, а передаёт каждый видеокадр в пользовательский код. Он действует как «выходная дверь» из пайплайна, позволяя приложению получать буферы с кадрами, анализировать их, модифицировать или перенаправлять дальше.

Ключевые особенности:

  • Работает как обычный sink, но вместо визуализации вызывает сигналы в вашем коде.
  • Поддерживает различные форматы кадров (RGB, BGR, YUV и др.).
  • Позволяет управлять поведением: сколько кадров хранить, что делать при переполнении, синхронизировать ли с таймстемпами.
  • Интегрируется с любым языком, где есть привязки к GStreamer (в том числе Python, C, Rust).

Пример пайплайна с appsink

Рассмотрим типичный пайплайн, который получает видео по RTSP, декодирует его и передаёт кадры в приложение:

rtspsrc location=rtsp://user:pass@192.168.1.100:554/stream latency=0 ! \
decodebin ! \
videoconvert ! \
video/x-raw,format=BGR ! \
appsink name=outsink emit-signals=true sync=false

Разберём его по частям:

Элемент / СекцияНазначение
rtspsrcПодключается к RTSP-источнику, управляет сигналингом и приёмом RTP-пакетов.
decodebinАвтоматически выбирает подходящий декодер (например, H.264) в зависимости от потока.
videoconvertПреобразует формат кадра в тот, который поддерживается следующими элементами.
video/x-raw,format=BGRФильтр по формату: указывает, что далее ожидается необработанный видеопоток в формате BGR (подходит для OpenCV).
appsinkПриёмник, передающий кадры в приложение. Параметры:
name=outsink — имя для доступа из кода;
emit-signals=true — включает сигнал new-sample;
sync=false — не ждёт синхронизации по времени, выдаёт кадры сразу.

⚠️ Обратите внимание: sync=false здесь критичен. Если оставить sync=true, приложение будет ждать точного времени отображения кадра, что создаёт искусственную задержку. Для анализа или обработки в реальном времени это нежелательно.


Как получить кадры в Python?

В Python через библиотеку PyGObject можно подписаться на сигнал new-sample, который appsink генерирует каждый раз, когда появляется новый кадр.

Шаги:

  1. Создать пайплайн с appsink.
  2. Подключить обработчик к сигналу new-sample.
  3. В обработчике извлечь буфер и преобразовать его в numpy-массив.
  4. Передать кадр в OpenCV или другую библиотеку.

Пример кода:

import gi
gi.require_version("Gst", "1.0")
gi.require_version("GstApp", "1.0")
from gi.repository import Gst, GstApp, GObject
import numpy as np

# Инициализация GStreamer
Gst.init(None)

# Строка пайплайна
pipeline_str = """
rtspsrc location=rtsp://user:pass@192.168.1.100:554/stream latency=0 !
decodebin !
videoconvert !
video/x-raw,format=BGR !
appsink name=outsink emit-signals=true sync=false
"""

# Создаём пайплайн
pipeline = Gst.parse_launch(pipeline_str)

# Получаем элемент appsink по имени
appsink = pipeline.get_by_name("outsink")

# Функция-обработчик нового кадра
def on_new_sample(sink):
# Получаем сэмпл (кадр) из appsink
sample = sink.emit("pull-sample")
if sample:
# Извлекаем буфер
buffer = sample.get_buffer()
# Получаем данные (в виде байтов)
data = buffer.extract_dup(0, buffer.get_size())

# Получаем информацию о формате
caps = sample.get_caps()
structure = caps.get_structure(0)
width = structure.get_value("width")
height = structure.get_value("height")
format_str = structure.get_value("format")

# Преобразуем байты в numpy-массив
# Для format=BGR: 3 канала, uint8
frame = np.frombuffer(data, dtype=np.uint8).reshape((height, width, 3))

# Теперь frame — полноценный OpenCV-кадр
# Пример: можно обработать его с помощью cv2
# import cv2
# cv2.imshow("Frame", frame)
# cv2.waitKey(1)

return Gst.FlowReturn.OK
return Gst.FlowReturn.ERROR

# Подключаем сигнал
appsink.connect("new-sample", on_new_sample)

# Запускаем пайплайн
pipeline.set_state(Gst.State.PLAYING)

# Обработка сообщений с bus (ошибки, конец потока)
bus = pipeline.get_bus()
try:
while True:
msg = bus.timed_pop_filtered(10 * Gst.MSECOND, Gst.MessageType.ERROR | Gst.MessageType.EOS)
if msg:
if msg.type == Gst.MessageType.ERROR:
err, debug = msg.parse_error()
print("Ошибка:", err, debug)
break
elif msg.type == Gst.MessageType.EOS:
print("Поток завершён")
break
except KeyboardInterrupt:
print("Остановлено пользователем")

# Завершаем
pipeline.set_state(Gst.State.NULL)

Что происходит внутри on_new-sample?

Когда appsink получает новый кадр, он:

  1. Сохраняет его во внутреннем буфере.
  2. Генерирует сигнал new-sample.
  3. Наша функция on_new_sample вызывается.
  4. Мы вызываем pull-sample, чтобы получить объект GstSample.
  5. Из GstSample извлекаем:
    • буфер с сырыми данными кадра;
    • caps — метаданные (размер, формат, частоту кадров).
  6. Данные преобразуются в numpy.ndarray — стандартный формат для OpenCV и других библиотек.

🔍 Визуализация:
Представьте, что appsink — это люк в днище движущегося поезда. Каждый вагон — это кадр. Когда поезд проезжает мимо, люк открывается (new-sample), и вы можете вытащить содержимое вагона (pull-sample). Затем вы можете его изучить, перекрасить, перегрузить в другой вагон — и всё это в реальном времени.


Почему format=BGR?

OpenCV по умолчанию использует порядок каналов BGR (синий-зелёный-красный), а не RGB. Поэтому важно указать:

video/x-raw,format=BGR

Если вы забудете это сделать, и GStreamer выдаст, например, format=RGB, то изображение в OpenCV будет выглядеть искажённым (например, красные объекты станут синими).

💡 Альтернатива: можно оставить format=RGB и использовать cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) — но это лишняя операция, снижающая производительность.


Возможные применения appsink

ПрименениеОписание
Анализ видео (OpenCV)Детекция лиц, движений, объектов, распознавание номеров и т.д.
Виртуальная камераПередача кадров в /dev/videoX через v4l2loopback для использования в Zoom, OBS и др.
Плагин для OBSИнтеграция GStreamer как источника через appsink + obs-python.
Сохранение с обработкойЗапись в файл, но с наложением оверлеев, водяных знаков или метаданных.
AI-инференсПередача кадров в модели YOLO, DeepSORT, MediaPipe и др.

Важные замечания

  • Производительность: если обработка кадра занимает больше времени, чем длится кадровый интервал (например, 33 мс при 30 FPS), appsink начнёт накапливать буферы. Это может привести к росту задержки и потребления памяти.

  • Решение: использовать параметр max-buffers и drop или leaky поведение:

    appsink max-buffers=2 drop=true

    Это ограничит очередь и будет отбрасывать старые кадры при перегрузке — полезно для реального времени.

  • Многопоточность: GStreamer автоматически обрабатывает поток в отдельном потоке. Ваш обработчик new-sample должен быть быстрым и неблокирующим.


Заключение

appsink превращает GStreamer из инструмента воспроизведения в гибкий вычислительный движок. Он позволяет:

  • Получать кадры в Python;
  • Интегрировать с OpenCV, AI-моделями, виртуальными камерами;
  • Строить сложные пайплайны, где видео — не конечная цель, а вход для обработки.

Это ключевой шаг к созданию интеллектуальных медиа-приложений, где GStreamer отвечает за приём и декодирование, а ваш код — за логику и анализ.