SIRINVIDEO Документация администратора и интегратора На сайт 2026.08

Готовые сценарии

Рабочие рецепты на curl и Python — добавить поток, завести сотню потоков из списка, снять статистику, выгрузить зрителей, включить архив, проверить здоровье.

Готовые к запуску сценарии. Каждый работает как есть — подставьте адрес сервера и пароль. Отдельные операции разобраны на страницах Потоки и Конфигурация и система, общие правила — во Введении.

Подготовка#

Все примеры на bash рассчитывают на две переменные:

BASE=https://media.example.com
TOKEN=$(curl -sS -X POST "$BASE/api/v1/auth/login" \
  -H 'Content-Type: application/json' \
  -d '{"password":"ваш-пароль-администратора"}' \
  | python3 -c 'import sys,json; print(json.load(sys.stdin)["token"])')
Для автоматизации возьмите статический токен вместо пароля

Сессионный токен живёт 12 часов, и скрипт, запускаемый по расписанию, будет входить заново каждый раз. Задайте auth { api_write_token "…"; } (или api_read_token для сценариев, которые только читают) и подставляйте его в TOKEN напрямую — вход не потребуется:

BASE=https://media.example.com
TOKEN='ваш-статический-токен-не-короче-16-символов'

Примеры на Python используют только стандартную библиотеку. Общая заготовка — сохраните её как sirin.py рядом со скриптами:

"""Минимальный клиент /api/v1 — только стандартная библиотека."""
import json
import urllib.error
import urllib.parse
import urllib.request

BASE = "https://media.example.com"


def api(path, token=None, method="GET", body=None, timeout=30):
    """Один запрос к /api/v1. Возвращает разобранный JSON."""
    req = urllib.request.Request(BASE + "/api/v1" + path, method=method)
    if token:
        req.add_header("Authorization", "Bearer " + token)
    data = None
    if body is not None:
        data = json.dumps(body).encode()
        req.add_header("Content-Type", "application/json")
    try:
        with urllib.request.urlopen(req, data, timeout=timeout) as resp:
            return json.load(resp)
    except urllib.error.HTTPError as e:
        detail = e.read().decode("utf-8", "replace")
        raise RuntimeError("%s %s → HTTP %d: %s" % (method, path, e.code, detail))


def login(password):
    """Вход по паролю администратора. Возвращает сессионный токен на 12 часов."""
    return api("/auth/login", method="POST", body={"password": password})["token"]


def sid(stream_id):
    """Идентификатор потока для URL: live/cam1 → live%2Fcam1."""
    return urllib.parse.quote(stream_id, safe="")

Добавить поток#

curl -sS -X POST "$BASE/api/v1/streams" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"path":"cam1",
       "directives":"input rtsp://admin:пароль@10.0.0.10:554/Streaming/Channels/101;"}'
{"epoch":12,"hash":"4a186afe976f6ef3","path":"cam1","changed":true}
Поток поднялся и принимает кадры
sleep 3
curl -sS "$BASE/api/v1/streams/cam1?fields=id,alive,health,fps,bitrate" \
  -H "Authorization: Bearer $TOKEN"
{"id":"cam1","alive":true,"health":"ok","fps":25.0,"bitrate":2920000.0}

Если пришло 404 — поток создан, но ещё не запустился либо источник недоступен; посмотрите GET /api/v1/logs?stream=cam1. Если health держится failing — проверьте адрес и учётные данные камеры.

Зрителю поток доступен сразу по всем протоколам:

http://media.example.com:8080/cam1/index.m3u8      HLS
http://media.example.com:8080/cam1/embed.html      тестовый плеер
rtsp://media.example.com:8554/cam1                 RTSP

Завести потоки из списка#

Типовая задача при вводе объекта: есть таблица камер — нужно сто потоков. Подготовьте cameras.csv:

path,url
lobby/cam1,rtsp://admin:пароль@10.0.0.10:554/Streaming/Channels/101
lobby/cam2,rtsp://admin:пароль@10.0.0.11:554/Streaming/Channels/101
gate/cam1,rtsp://admin:пароль@10.0.0.12:554/Streaming/Channels/101
#!/usr/bin/env python3
"""Массовое создание потоков из CSV. Повторный запуск безопасен."""
import csv
import sys
import time

from sirin import api, login, sid

PASSWORD = "ваш-пароль-администратора"
RECORD = True          # писать архив
STORAGE = "local"      # имя хранилища из конфигурации
RETENTION = "7d 500G"  # глубина: по времени и по объёму

token = login(PASSWORD)

# Что уже есть — чтобы повторный запуск не падал на 409.
existing = {row["path"] for row in api("/config/streams", token)}

created = skipped = failed = 0
with open("cameras.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        path, url = row["path"].strip(), row["url"].strip()
        if not path or not url:
            continue
        if path in existing:
            print("= %s уже настроен" % path)
            skipped += 1
            continue

        directives = "input %s;" % url
        if RECORD:
            directives += "\n  dvr %s %s;" % (STORAGE, RETENTION)

        try:
            res = api("/streams", token, method="POST",
                      body={"path": path, "directives": directives})
            print("+ %s создан (версия %d)" % (path, res["epoch"]))
            created += 1
        except RuntimeError as e:
            print("! %s: %s" % (path, e), file=sys.stderr)
            failed += 1

        # Каждое создание переписывает и применяет конфигурацию — не гоните очередь.
        time.sleep(0.2)

print("\nсоздано %d, пропущено %d, с ошибкой %d" % (created, skipped, failed))
+ lobby/cam1 создан (версия 12)
+ lobby/cam2 создан (версия 13)
= gate/cam1 уже настроен

создано 2, пропущено 1, с ошибкой 0
Каждое создание — это отдельная запись конфигурации

POST /streams переписывает файл и применяет его целиком. Для сотни камер это сто применений: пауза между запросами обязательна, а параллельно запускать такой скрипт нельзя — вторая копия получит 409. Если камер тысячи, соберите весь текст конфигурации сами и примените один раз через PUT /config.

Одинаковые настройки выносите в шаблон

Если у всех камер совпадают параметры записи и упаковки, опишите их один раз шаблоном (ConfigurationTemplates) и ссылайтесь на него из блока потока — тогда в directives останется только input.

Снять статистику#

Сводка по узлу одной командой:

curl -sS "$BASE/api/v1/totals" -H "Authorization: Bearer $TOKEN" \
 | python3 -c '
import sys, json
t = json.load(sys.stdin)
print("потоков:        %d" % t["streams"])
print("зрителей:       %d (живых %d, архив %d)"
      % (t["sessions"], t["live_sessions"], t["dvr_sessions"]))
print("принято:        %.1f ГиБ" % (t["bytes_in"] / 2**30))
print("отдано:         %.1f ГиБ (архив %.1f ГиБ)"
      % (t["bytes_out"] / 2**30, t["dvr_bytes_out"] / 2**30))
'
потоков:        10
зрителей:       1 (живых 1, архив 0)
принято:        6.1 ГиБ
отдано:         0.1 ГиБ (архив 0.0 ГиБ)

Таблица по потокам, отсортированная по числу зрителей:

curl -sS "$BASE/api/v1/streams?limit=50000&sort=viewers:desc&fields=id,health,viewers,bitrate,fps,reconnects,input_errors" \
  -H "Authorization: Bearer $TOKEN" \
 | python3 -c '
import sys, json
d = json.load(sys.stdin)
head = "%-28s %-10s %6s %8s %6s %11s %7s"
row  = "%-28s %-10s %6d %8.2f %6.1f %11d %7d"
print(head % ("поток", "здоровье", "зрит.", "Мбит/с", "fps", "переподкл.", "ошибок"))
for s in d["items"]:
    print(row % (s["id"], s["health"], s["viewers"], s["bitrate"] / 1e6,
                 s["fps"], s["reconnects"], s["input_errors"]))
print("\nвсего потоков: %d" % d["total"])
'
поток                        здоровье    зрит.   Мбит/с    fps  переподкл.  ошибок
cam1                         ok              2     3.73   25.0           0       0
cam2                         ok              1     1.37   25.1           0       0
gate/cam1                    failing         0     0.00    0.0          13      13

всего потоков: 3

Ресурсы хоста:

curl -sS "$BASE/api/v1/system" -H "Authorization: Bearer $TOKEN" \
 | python3 -c '
import sys, json
s = json.load(sys.stdin)
print("CPU процесса:   %.2f ядра из %d рабочих" % (s["proc_cpu_cores"], s["worker_cores"]))
print("CPU хоста:      %.1f%%" % s["host_cpu_busy_pct"])
print("память:         %d МиБ RSS из %d МиБ" % (s["rss_mb"], s["mem_total_mb"]))
print("дескрипторов:   %d" % s["open_fds"])
print("аптайм:         %.1f ч" % (s["uptime_s"] / 3600))
'

Выгрузить список зрителей#

curl -sS "$BASE/api/v1/sessions?limit=50000&fields=stream,protocol,client_ip,user_agent,bytes_out,connected_ms,rr_fraction_lost" \
  -H "Authorization: Bearer $TOKEN" \
 | python3 -c '
import sys, json, csv, datetime
d = json.load(sys.stdin)
w = csv.writer(sys.stdout)
w.writerow(["поток", "протокол", "адрес", "отдано_байт", "потери_%", "подключён", "клиент"])
for s in d["items"]:
    when = datetime.datetime.fromtimestamp(s["connected_ms"] / 1000)
    w.writerow([s["stream"], s["protocol"], s.get("client_ip") or "",
                s["bytes_out"], round(s["rr_fraction_lost"] * 100 / 256, 1),
                when.isoformat(timespec="seconds"), s.get("user_agent") or ""])
' > viewers.csv
поток,протокол,адрес,отдано_байт,потери_%,подключён,клиент
cam1,hls-fmp4,10.0.0.55,109546034,0.0,2026-08-19T17:56:05,Mozilla/5.0 …
cam1,dvr,10.0.0.61,54428222,0.0,2026-08-19T17:58:47,Mozilla/5.0 …
Список — это моментальный снимок

GET /sessions показывает активные сессии. Ушедший зритель исчезает из списка бесследно. Для учёта всех просмотров подпишитесь на тему sessions-audit потока событий и складывайте события в своё хранилище — см. Мониторинг и метрики.

Только проблемные зрители — те, у кого есть потери или задержки доставки:

curl -sS "$BASE/api/v1/sessions?min_loss=1&fields=stream,protocol,client_ip,rr_fraction_lost,lag_events" \
  -H "Authorization: Bearer $TOKEN"

Включить архив у существующего потока#

Правка потока заменяет тело блока целиком, поэтому нельзя просто дописать строку — нужно прочитать текущие директивы, добавить свою и отправить всё обратно.

#!/usr/bin/env python3
"""Включить запись архива у потока, не потеряв остальные его директивы."""
from sirin import api, login, sid

PASSWORD = "ваш-пароль-администратора"
STREAM = "cam1"
STORAGE = "local"
RETENTION = "7d 500G"

token = login(PASSWORD)

# 1. Текущее тело блока — работает и для остановленного потока.
cur = api("/streams/%s/config" % sid(STREAM), token)
directives = cur["directives"].rstrip()
print("было:\n%s\n" % directives)

if "dvr " in directives:
    raise SystemExit("у потока %s архив уже настроен — правьте вручную" % STREAM)

# 2. Дописываем одну директиву к существующему телу.
directives += "\n  dvr %s %s;" % (STORAGE, RETENTION)

# 3. Отправляем тело целиком.
res = api("/streams/%s" % sid(STREAM), token, method="PATCH",
          body={"directives": directives})
print("стало:\n%s\n" % directives)
print("применено, версия конфигурации %d" % res["epoch"])
было:
input rtsp://admin:пароль@10.0.0.10:554/Streaming/Channels/101;

стало:
input rtsp://admin:пароль@10.0.0.10:554/Streaming/Channels/101;
  dvr local 7d 500G;

применено, версия конфигурации 14
Запись действительно идёт
curl -sS "$BASE/api/v1/storages" -H "Authorization: Bearer $TOKEN" \
 | python3 -c '
import sys, json
for st in json.load(sys.stdin):
    print("%s: занято %.1f ГиБ" % (st["name"], st["used_bytes"] / 2**30))
    for path, files, used in st["streams"]:
        print("   %-28s %6d сегм.  %.2f ГиБ" % (path, files, used / 2**30))
'

Поток должен появиться в списке хранилища в течение минуты. Если его нет — убедитесь, что хранилище с таким именем существует, и загляните в GET /api/v1/logs?stream=cam1.

Хранилище должно существовать заранее

Директива dvr ссылается на блок storage по имени. Если такого блока нет, правка будет отклонена кодом 422 и на диск не попадёт — это защита, а не сбой. Как завести хранилище — Хранилища и S3.

Проверка здоровья для системы мониторинга#

Скрипт возвращает коды выхода, принятые в системах мониторинга: 0 — норма, 1 — предупреждение, 2 — авария.

#!/bin/sh
# health.sh — проверка узла SirinVideo.
# Переменные окружения: BASE, TOKEN (достаточно api_read_token).
set -eu

counts=$(curl -sS --max-time 10 "$BASE/api/v1/streams/counts" \
           -H "Authorization: Bearer $TOKEN") \
  || { echo "CRITICAL: сервер недоступен"; exit 2; }

field() { printf '%s' "$counts" | python3 -c "import sys,json;print(json.load(sys.stdin)$1)"; }

total=$(field '["total"]')
failing=$(field '["health"]["failing"]')
degraded=$(field '["health"]["degraded"]')

if [ "$failing" -gt 0 ]; then
  echo "CRITICAL: потоков в отказе $failing из $total"; exit 2
fi
if [ "$degraded" -gt 0 ]; then
  echo "WARNING: деградировавших потоков $degraded из $total"; exit 1
fi
echo "OK: все $total потоков здоровы"; exit 0
BASE=https://media.example.com TOKEN='ваш-токен-чтения' ./health.sh; echo "код выхода $?"
OK: все 10 потоков здоровы
код выхода 0

Более подробная проверка — с учётом лицензии, места на дисках и молчащих источников:

#!/usr/bin/env python3
"""Развёрнутая проверка узла. Код выхода: 0 норма, 1 предупреждение, 2 авария."""
import sys

from sirin import api

TOKEN = "ваш-токен-чтения"     # достаточно api_read_token
NO_DATA_LIMIT = 30             # секунд молчания источника до тревоги
DISK_LIMIT = 95                # процент заполнения диска до тревоги

warn, crit = [], []

try:
    lic = api("/license", TOKEN)
    if lic["state"] == "grace":
        warn.append("лицензия не обновляется: %s" % lic.get("reason", "причина не указана"))
    elif lic["state"] != "licensed":
        crit.append("лицензия недействительна (%s) — медиа не отдаётся" % lic["state"])

    counts = api("/streams/counts", TOKEN)
    if counts["health"]["failing"]:
        crit.append("потоков в отказе: %d" % counts["health"]["failing"])
    if counts["health"]["degraded"]:
        warn.append("деградировавших потоков: %d" % counts["health"]["degraded"])

    quiet = [s["id"] for s in api("/streams?fields=id,no_data_secs&limit=50000", TOKEN)["items"]
             if s.get("no_data_secs", 0) > NO_DATA_LIMIT]
    if quiet:
        crit.append("нет данных от источников: %s" % ", ".join(sorted(quiet)[:5]))

    for st in api("/storages", TOKEN):
        if not st["total_bytes"]:
            continue
        pct = 100 * st["used_bytes"] / st["total_bytes"]
        if pct >= DISK_LIMIT:
            warn.append("хранилище %s заполнено на %.0f%%" % (st["name"], pct))
        if not st["identity_healthy"] or not st["maintenance_healthy"]:
            crit.append("хранилище %s не обслуживается" % st["name"])
        if not st["s3_healthy"]:
            warn.append("ярус S3 хранилища %s недоступен" % st["name"])

except Exception as e:                      # сеть, 401, 5xx
    print("CRITICAL: %s" % e)
    sys.exit(2)

if crit:
    print("CRITICAL: " + "; ".join(crit + warn))
    sys.exit(2)
if warn:
    print("WARNING: " + "; ".join(warn))
    sys.exit(1)
print("OK: %d потоков, лицензия в порядке, хранилища в норме" % counts["total"])
sys.exit(0)
OK: 10 потоков, лицензия в порядке, хранилища в норме
Метрики лучше собирать не скриптом

Скрипт-проверка отвечает на вопрос «всё ли хорошо прямо сейчас». Для графиков и истории поднимите сбор с эндпоинта GET /metrics — там те же данные в формате Prometheus, включая ряды по каждому потоку. См. Мониторинг и метрики.

Куда дальше#