refactor: remove SaluteJazz carrier support

This commit is contained in:
zarazaex69
2026-05-19 21:39:07 +03:00
parent d84fb78eef
commit 085aadcad7
36 changed files with 97 additions and 2866 deletions

View File

@@ -87,7 +87,8 @@ func TestRunWithConfigValidationAndDataDirErrors(t *testing.T) {
scfg := session.Config{
Mode: "srv",
Transport: "datachannel",
Auth: "jazz",
Auth: "jitsi",
RoomID: "https://meet.small-dm.ru/test",
KeyHex: "key",
DNSServer: "1.1.1.1:53",
}
@@ -117,7 +118,7 @@ func TestRunWithArgsSuccessfulSessionReturn(t *testing.T) {
called := false
runSession = func(ctx context.Context, cfg session.Config) error {
called = true
if cfg.Mode != "srv" || cfg.Auth != "jazz" {
if cfg.Mode != "srv" || cfg.Auth != "jitsi" {
t.Fatalf("session config = %+v", cfg)
}
select {
@@ -132,7 +133,9 @@ func TestRunWithArgsSuccessfulSessionReturn(t *testing.T) {
mode: srv
link: direct
auth:
provider: jazz
provider: jitsi
room:
id: https://meet.small-dm.ru/test
crypto:
key: key
net:

View File

@@ -1,59 +0,0 @@
#!/usr/bin/env python3
import asyncio
import json
import uuid
import aiohttp
API_BASE = "https://bk.salutejazz.ru"
JAZZ_HEADERS = {"X-Jazz-ClientId": str(uuid.uuid4()), "X-Jazz-AuthType": "ANONYMOUS", "X-Client-AuthType": "ANONYMOUS", "Content-Type": "application/json"}
async def get_jazz_info():
print("\n--- SaluteJazz Info ---")
timeout = aiohttp.ClientTimeout(total=15)
async with aiohttp.ClientSession(timeout=timeout) as session:
print("[1/4] API Initialization...")
try:
r = await session.post(f"{API_BASE}/room/create-meeting", headers=JAZZ_HEADERS, json={"title": "InfoBot", "guestEnabled": True, "lobbyEnabled": False, "room3dEnabled": False})
rj = await r.json()
print(" :P Room created")
print(json.dumps(rj, indent=2))
r2 = await session.post(f"{API_BASE}/room/{rj['roomId']}/preconnect", headers=JAZZ_HEADERS, json={"password": rj["password"], "jazzNextMigration": {"b2bBaseRoomSupport": True, "sdkRoomSupport": True, "mediaWithoutAutoSubscribeSupport": True}})
r2j = await r2.json()
print(" :P Preconnect info received")
print(json.dumps(r2j, indent=2))
conn_url = r2j['connectorUrl']
except Exception as e:
print(f" X Error: {e}"); return
print(f"\n[2/4] Connecting to signaling...")
async with session.ws_connect(conn_url) as ws:
await ws.send_json({"roomId": rj["roomId"], "event": "join", "requestId": str(uuid.uuid4()), "payload": {"password": rj["password"], "participantName": "InfoBot", "supportedFeatures": {"attachedRooms": True}, "isSilent": False}})
print(" :P Signaling established")
print("\n[3/4] Collecting network & media details...")
end = asyncio.get_event_loop().time() + 8
while asyncio.get_event_loop().time() < end:
try:
m = await asyncio.wait_for(ws.receive(), 1)
if m.type == aiohttp.WSMsgType.TEXT:
d = json.loads(m.data); ev = d.get("event", ""); p = d.get("payload", {}); meth = p.get("method", "")
print(f" -> Event: {ev}{' ('+meth+')' if meth else ''}")
if meth == "rtc:config":
print("\n--- ICE Servers ---")
print(json.dumps(p.get("configuration", {}).get("iceServers", []), indent=2))
elif meth == "rtc:offer":
print("\n--- SDP Offer (Codecs & Quality) ---")
print(p.get("description", {}).get("sdp", ""))
elif ev == "join-response":
print("\n--- Participant Group ---")
print(json.dumps(p.get("participantGroup", {}), indent=2))
else:
print(json.dumps(p, indent=2))
except: continue
print("\n--- INFO COLLECTION COMPLETE ---")
if __name__ == "__main__":
try: asyncio.run(get_jazz_info())
except KeyboardInterrupt: pass

View File

@@ -1,241 +0,0 @@
#!/usr/bin/env python3
"""PoC: SaluteJazz DataChannel over LiveKit."""
import asyncio
import io
import json
import logging
import time
import uuid
import aiohttp
from aiortc import RTCConfiguration, RTCIceCandidate, RTCIceServer, RTCPeerConnection, RTCSessionDescription
from aiortc.mediastreams import AudioStreamTrack
from aiortc.rtcconfiguration import RTCBundlePolicy
logging.getLogger("aiortc").setLevel(logging.WARNING)
API_BASE = "https://bk.salutejazz.ru"
JAZZ_HEADERS = {"X-Jazz-ClientId": str(uuid.uuid4()), "X-Jazz-AuthType": "ANONYMOUS", "X-Client-AuthType": "ANONYMOUS", "Content-Type": "application/json"}
TEST_MESSAGES = ["Hello Jazz DC!", "Hello world", "X" * 100, "Final test"]
def _pb_varint(v: int) -> bytes:
b = bytearray()
while v > 0x7F: b.append((v & 0x7F) | 0x80); v >>= 7
b.append(v & 0x7F)
return bytes(b)
def _pb_field(f: int, w: int, d: bytes) -> bytes:
t = _pb_varint((f << 3) | w)
return t + d if w == 0 else (t + _pb_varint(len(d)) + d if w == 2 else t + d)
def _read_varint(s: io.BytesIO) -> int | None:
res, shift = 0, 0
while b := s.read(1):
res |= (b[0] & 0x7F) << shift
if not (b[0] & 0x80): return res
shift += 7
return None
def encode_data_packet(payload: bytes, topic: str = "") -> bytes:
uf = _pb_field(2, 2, payload) + (_pb_field(4, 2, topic.encode()) if topic else b"") + _pb_field(8, 2, str(uuid.uuid4()).encode())
return _pb_field(1, 0, _pb_varint(0)) + _pb_field(2, 2, uf)
def decode_data_packet(raw: bytes) -> tuple[bytes, str] | None:
s = io.BytesIO(raw)
ud = None
while (tg := _read_varint(s)) is not None:
wt = tg & 0x07
if wt == 0: _read_varint(s)
elif wt == 2:
l = _read_varint(s)
if l is None: break
d = s.read(l)
if (tg >> 3) == 2: ud = d
elif wt == 1: s.read(8)
elif wt == 5: s.read(4)
else: break
if ud is None: return None
p, t, ins = b"", "", io.BytesIO(ud)
while (tg := _read_varint(ins)) is not None:
wt = tg & 0x07
if wt == 0: _read_varint(ins)
elif wt == 2:
l = _read_varint(ins)
if l is None: break
d = ins.read(l)
fn = tg >> 3
if fn == 2: p = d
elif fn == 4: t = d.decode(errors="replace")
elif wt == 1: ins.read(8)
elif wt == 5: ins.read(4)
else: break
return p, t
async def _create_peer(name: str, room: dict, session: aiohttp.ClientSession, is_server: bool = False, stats: dict = None) -> dict:
ws = await session.ws_connect(room["connectorUrl"])
await ws.send_json({"roomId": room["roomId"], "event": "join", "requestId": str(uuid.uuid4()), "payload": {"password": room["password"], "participantName": name, "supportedFeatures": {"attachedRooms": True, "sessionGroups": True}, "isSilent": False}})
peer = {"ws": ws, "pc_sub": None, "pc_pub": None, "dc": None, "ready": asyncio.Event(), "sub_ready": asyncio.Event()}
group_id, p_ice_sub, p_ice_pub = None, [], []
ice_servers = []
async def ws_loop():
nonlocal group_id
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
data = json.loads(msg.data)
ev = data.get("event", "")
p = data.get("payload", {})
m = p.get("method", "")
if ev == "join-response": group_id = p.get("participantGroup", {}).get("groupId")
elif ev == "media-out" and m == "rtc:config":
for s in p.get("configuration", {}).get("iceServers", []):
urls = [u for u in s.get("urls", []) if "transport=udp" in u]
if urls: ice_servers.append(RTCIceServer(urls=[urls[0]], username=s.get("username"), credential=s.get("credential")))
elif ev == "media-out" and m == "rtc:offer" and not peer["pc_sub"]:
peer["pc_sub"] = RTCPeerConnection(configuration=RTCConfiguration(iceServers=ice_servers, bundlePolicy=RTCBundlePolicy.MAX_BUNDLE))
@peer["pc_sub"].on("connectionstatechange")
def _():
if peer["pc_sub"].connectionState == "connected": peer["sub_ready"].set()
@peer["pc_sub"].on("datachannel")
def on_dc(ch):
if ch.label != "_reliable": return
@ch.on("message")
def on_msg(msg_data):
parsed = decode_data_packet(msg_data if isinstance(msg_data, bytes) else msg_data.encode())
if not parsed or parsed[1] != "poc": return
stats["recv"] += 1
if is_server and peer["dc"]:
try:
peer["dc"].send(encode_data_packet(f"Echo: {parsed[0].decode()}".encode(), "poc"))
stats["sent"] += 1
except: pass
@peer["pc_sub"].on("icecandidate")
async def on_sub_ice(e):
if e and e.candidate and group_id:
await ws.send_json({"roomId": room["roomId"], "event": "media-in", "groupId": group_id, "requestId": str(uuid.uuid4()), "payload": {"method": "rtc:ice", "rtcIceCandidates": [{"candidate": e.candidate.candidate, "sdpMid": e.candidate.sdpMid, "sdpMLineIndex": e.candidate.sdpMLineIndex, "usernameFragment": "", "target": "SUBSCRIBER"}]}})
await peer["pc_sub"].setRemoteDescription(RTCSessionDescription(sdp=p["description"]["sdp"], type="offer"))
ans = await peer["pc_sub"].createAnswer()
await peer["pc_sub"].setLocalDescription(ans)
await ws.send_json({"roomId": room["roomId"], "event": "media-in", "groupId": group_id, "requestId": str(uuid.uuid4()), "payload": {"method": "rtc:answer", "description": {"type": "answer", "sdp": peer["pc_sub"].localDescription.sdp}}})
for c in p_ice_sub:
pts = c.get("candidate","").split()
if len(pts) >= 8: await peer["pc_sub"].addIceCandidate(RTCIceCandidate(int(pts[1]), pts[0].split(":")[1], pts[4], int(pts[5]), int(pts[3]), pts[2], pts[7], str(c.get("sdpMid", "0")), c.get("sdpMLineIndex", 0)))
p_ice_sub.clear()
await asyncio.sleep(0.3)
peer["pc_pub"] = RTCPeerConnection(configuration=RTCConfiguration(iceServers=ice_servers, bundlePolicy=RTCBundlePolicy.MAX_BUNDLE))
peer["pc_pub"].addTrack(AudioStreamTrack())
peer["dc"] = peer["pc_pub"].createDataChannel("_reliable", ordered=True)
@peer["dc"].on("open")
def on_open(): peer["ready"].set()
@peer["dc"].on("message")
def on_pub_msg(msg_data):
parsed = decode_data_packet(msg_data if isinstance(msg_data, bytes) else msg_data.encode())
if parsed and parsed[1] == "poc": stats["recv"] += 1
@peer["pc_pub"].on("icecandidate")
async def on_pub_ice(e):
if e and e.candidate and group_id:
await ws.send_json({"roomId": room["roomId"], "event": "media-in", "groupId": group_id, "requestId": str(uuid.uuid4()), "payload": {"method": "rtc:ice", "rtcIceCandidates": [{"candidate": e.candidate.candidate, "sdpMid": e.candidate.sdpMid, "sdpMLineIndex": e.candidate.sdpMLineIndex, "usernameFragment": "", "target": "PUBLISHER"}]}})
await ws.send_json({"roomId": room["roomId"], "event": "media-in", "groupId": group_id, "requestId": str(uuid.uuid4()), "payload": {"method": "rtc:track:add", "cid": str(uuid.uuid4()), "track": {"type": "AUDIO", "source": "MICROPHONE", "muted": True}}})
pub_offer = await peer["pc_pub"].createOffer()
await peer["pc_pub"].setLocalDescription(pub_offer)
await ws.send_json({"roomId": room["roomId"], "event": "media-in", "groupId": group_id, "requestId": str(uuid.uuid4()), "payload": {"method": "rtc:offer", "description": {"type": "offer", "sdp": peer["pc_pub"].localDescription.sdp}}})
elif ev == "media-out" and m == "rtc:answer" and peer["pc_pub"]:
await peer["pc_pub"].setRemoteDescription(RTCSessionDescription(sdp=p["description"]["sdp"], type="answer"))
for c in p_ice_pub:
pts = c.get("candidate","").split()
if len(pts) >= 8: await peer["pc_pub"].addIceCandidate(RTCIceCandidate(int(pts[1]), pts[0].split(":")[1], pts[4], int(pts[5]), int(pts[3]), pts[2], pts[7], str(c.get("sdpMid", "0")), c.get("sdpMLineIndex", 0)))
p_ice_pub.clear()
elif ev == "media-out" and m == "rtc:ice":
for c in p.get("rtcIceCandidates", []):
pts = c.get("candidate","").split()
if len(pts) < 8: continue
ice = RTCIceCandidate(int(pts[1]), pts[0].split(":")[1], pts[4], int(pts[5]), int(pts[3]), pts[2], pts[7], str(c.get("sdpMid", "0")), c.get("sdpMLineIndex", 0))
tgt = c.get("target")
if tgt == "SUBSCRIBER": (await peer["pc_sub"].addIceCandidate(ice)) if peer["pc_sub"] else p_ice_sub.append(c)
elif tgt == "PUBLISHER": (await peer["pc_pub"].addIceCandidate(ice)) if peer["pc_pub"] else p_ice_pub.append(c)
async def _keep():
while not ws.closed:
await asyncio.sleep(5)
if group_id: await ws.send_json({"roomId": room["roomId"], "event": "media-in", "groupId": group_id, "requestId": str(uuid.uuid4()), "payload": {"method": "rtc:ping", "ping_req": {"timestamp": int(time.time()*1000), "rtt": 0}}})
peer["task"] = asyncio.create_task(ws_loop())
peer["keep"] = asyncio.create_task(_keep())
return peer
async def run_poc() -> dict:
print("\n--- SaluteJazz PoC ---")
results = {"server_ok": False, "client_ok": False, "sent": 0, "recv": 0, "errors": []}
s_stats, c_stats = {"sent": 0, "recv": 0}, {"sent": 0, "recv": 0}
async with aiohttp.ClientSession() as session:
try:
r = await session.post(f"{API_BASE}/room/create-meeting", headers=JAZZ_HEADERS, json={"title": "PoC", "guestEnabled": True, "lobbyEnabled": False})
rj = await r.json()
r2 = await session.post(f"{API_BASE}/room/{rj['roomId']}/preconnect", headers=JAZZ_HEADERS, json={"password": rj["password"], "jazzNextMigration": {"b2bBaseRoomSupport": True, "demoRoomBaseSupport": True, "demoRoomVersionSupport": 2, "mediaWithoutAutoSubscribeSupport": True}})
room_inf = {"roomId": rj["roomId"], "password": rj["password"], "connectorUrl": (await r2.json())["connectorUrl"]}
except Exception as e:
results["errors"].append(f"Auth fail: {e}")
return results
print("[1/3] Connecting Server & Client...")
try:
server = await _create_peer("Server", room_inf, session, is_server=True, stats=s_stats)
await asyncio.wait_for(server["ready"].wait(), 15.0)
results["server_ok"] = True
client = await _create_peer("Client", room_inf, session, is_server=False, stats=c_stats)
await asyncio.wait_for(client["ready"].wait(), 15.0)
results["client_ok"] = True
print(" :P Peers connected")
except Exception as e:
results["errors"].append(str(e))
return results
print("\n[2/3] Exchanging messages...")
await asyncio.sleep(1)
for idx, msg in enumerate(TEST_MESSAGES, 1):
try:
client["dc"].send(encode_data_packet(msg.encode(), "poc"))
c_stats["sent"] += 1
print(f" -> Sent: {msg}")
await asyncio.sleep(0.5)
except Exception as e:
results["errors"].append(f"Sending {idx} failed: {str(e)}")
await asyncio.sleep(3)
results["sent"], results["recv"] = c_stats["sent"], c_stats["recv"]
print("\n[3/3] Cleaning up...")
for p in (server, client):
for t in ["task", "keep"]: p[t].cancel()
await p["ws"].close()
for pc in [p["pc_sub"], p["pc_pub"]]:
if pc: await pc.close()
return results
def print_results(res: dict):
print("\n--- TEST RESULTS ---")
print(f"Server: {':P' if res['server_ok'] else 'X'} / Client: {':P' if res['client_ok'] else 'X'}")
print(f"Messages: Sent {res['sent']} / Recv {res['recv']}")
if res['errors']:
for e in res['errors']: print(f" Error: {e}")
print(f"\n{':P SUCCESS' if res['sent'] and res['sent'] == res['recv'] else 'X FAILED'}\n")
if __name__ == "__main__":
try: res = asyncio.run(run_poc()); print_results(res)
except KeyboardInterrupt: pass

View File

@@ -8,7 +8,7 @@ services:
network_mode: host
environment:
OLCRTC_MODE: cnc
OLCRTC_CARRIER: "${OLCRTC_CARRIER:?set OLCRTC_CARRIER (jitsi, telemost, jazz, wbstream, none)}"
OLCRTC_CARRIER: "${OLCRTC_CARRIER:?set OLCRTC_CARRIER (jitsi, telemost, wbstream, none)}"
OLCRTC_TRANSPORT: "${OLCRTC_TRANSPORT:-datachannel}"
OLCRTC_ROOM_ID: "${OLCRTC_ROOM_ID:?set OLCRTC_ROOM_ID to the server room}"
OLCRTC_KEY: "${OLCRTC_KEY:?set OLCRTC_KEY to the server encryption key}"

View File

@@ -7,7 +7,7 @@ services:
restart: unless-stopped
environment:
OLCRTC_MODE: srv
OLCRTC_CARRIER: "${OLCRTC_CARRIER:?set OLCRTC_CARRIER (jitsi, telemost, jazz, wbstream, none)}"
OLCRTC_CARRIER: "${OLCRTC_CARRIER:?set OLCRTC_CARRIER (jitsi, telemost, wbstream, none)}"
OLCRTC_TRANSPORT: "${OLCRTC_TRANSPORT:-datachannel}"
OLCRTC_ROOM_ID: "${OLCRTC_ROOM_ID:-}"
OLCRTC_KEY: "${OLCRTC_KEY:-}"

View File

@@ -43,12 +43,12 @@
Классические обходы через VPS ломаются когда VPS не попадает в белый список. Yandex Cloud, VK Cloud, Timeweb в списке - но провайдеры активно банят инстансы используемые как прокси.
**Решение olcRTC**: не пытаться попасть в белый список - использовать сервисы, которые там уже есть навсегда. Телемост, SaluteJazz и WB Stream - сервисы видеозвонков крупных российских компаний. Пока они живы, olcRTC работает. Чтобы их заблокировать - нужно заблокировать сам сервис.
**Решение olcRTC**: не пытаться попасть в белый список - использовать сервисы, которые там уже есть навсегда. Телемост и WB Stream - сервисы видеозвонков крупных российских компаний. Пока они живы, olcRTC работает. Чтобы их заблокировать - нужно заблокировать сам сервис.
Трафик идёт через WebRTC SFU этих сервисов:
```
Клиент (cnc) → SFU Яндекса/Сбера/WB → Сервер (srv, ваш VPS)
Клиент (cnc) → SFU Яндекса/WB → Сервер (srv, ваш VPS)
```
Для ТСПУ это выглядит как обычный видеозвонок.
@@ -79,9 +79,9 @@
**2026-04-08..09** - активная Go разработка: клиент-серверная архитектура, кастомный мультиплексор с sequence numbering, имена участников из файла, graceful shutdown, DNS поддержка, Android мост.
**2026-04-10..11** - простой UI, Docker образ сервера, SaluteJazz PoC от community-контрибутора `0xcodepunk`.
**2026-04-10..11** - простой UI, Docker образ сервера.
**2026-04-12..14** - большой рефакторинг: golangci-lint, Jazz провайдер с protobuf-style пакетами, автогенерация Room ID для Jazz, Windows скрипты от `DeNcHiK3713`.
**2026-04-12..14** - большой рефакторинг: golangci-lint, Windows скрипты от `DeNcHiK3713`.
**2026-04-19..20** - архитектурный рефакторинг: выделение слоёв `carrier` / `transport` / `link`, WB Stream провайдер через LiveKit SDK, видеоканальный PoC на Python.
@@ -95,8 +95,8 @@
**2026-05-11..14** - большой архитектурный рефакторинг `refactor/universal-carrier`:
- Разделение `internal/provider/` на `internal/engine/` (wire-level SFU протоколы) + `internal/auth/` (HTTP/API авторизация)
- Три engine: `livekit` (WB Stream), `goolom` (Telemost), `salutejazz` (Jazz)
- Три auth: `wbstream`, `telemost`, `salutejazz`
- Два основных engine: `livekit` (WB Stream), `goolom` (Telemost)
- Auth-провайдеры: `wbstream`, `telemost`, `jitsi`
- Замена `-carrier` на `-auth`/`-engine`/`-url`/`-token`
- Публичный Go API `pkg/olcrtc` (net.Conn через Session.Dial) для встраивания в sing-box и другие
- `cmd/olcrtc-cgo` — C-shared библиотека с Ping API
@@ -104,7 +104,6 @@
- Протокол handshake (`internal/handshake/`) с CLIENT_HELLO/SERVER_WELCOME
- Session callbacks: OnSessionOpen, OnSessionClose, OnTraffic
- Перевод документации на русский
- E2E тесты: jazz non-data транспорты помечены как expected fail
### Статья на Хабре
@@ -129,10 +128,10 @@
Transport (datachannel / vp8channel / seichannel / videochannel)
Carrier (jazz / wbstream / telemost)
Carrier (wbstream / telemost / jitsi)
│ WebRTC DataChannel или VideoTrack
SFU Яндекса / Сбера / WB ← сервер в белом списке у всех провайдеров
SFU Яндекса / WB / Jitsi ← сервер в белом списке у всех провайдеров
Transport (datachannel / vp8channel / seichannel / videochannel)
@@ -148,7 +147,7 @@
Сервер (`srv`) стоит на вашем VPS. Он подключается к той же комнате видеозвонка, получает зашифрованный поток и от своего имени делает TCP соединения к нужным адресам в интернете.
ТСПУ видит трафик к IP Яндекса/Сбера/WB с корректным TLS и SNI - ничем не отличается от обычного видеозвонка.
ТСПУ видит трафик к IP выбранного сервиса с корректным TLS и SNI - ничем не отличается от обычного видеозвонка.
---
@@ -185,12 +184,12 @@ internal/carrier/ интерфейс Carrier + реестр
internal/engine/ Wire-level SFU протоколы (URL+Token → WebRTC)
├── livekit/ LiveKit (WB Stream)
├── goolom/ Goolom (Yandex Telemost)
└── salutejazz/ SaluteJazz (Сбер)
└── jitsi/ Jitsi Meet
internal/auth/ HTTP/API авторизация → Credentials для engine
├── wbstream/ WB Stream API (guest register, join, token)
├── telemost/ Yandex Telemost (connection-info)
└── salutejazz/ SaluteJazz (create-meeting, preconnect)
└── jitsi/ Jitsi room URL parsing
internal/crypto/ ChaCha20-Poly1305 AEAD
internal/names/ генератор имён участников
@@ -305,7 +304,7 @@ internal/e2e/ E2E тесты на реальных провайдер
| `carrier.go` | Интерфейс `Session` + реестр. `Capabilities` описывает что умеет carrier: ByteStream и/или VideoTrack |
| `bytestream.go` | `ByteStream` и `VideoTrack` интерфейсы |
| `carrier_test.go` | Тесты |
| `builtin/register.go` | Регистрирует jazz, telemost, wbstream, none в реестре carrier через `registerEngineAuth` (связывает auth provider с engine) и `registerDirect` (прямое подключение без auth) |
| `builtin/register.go` | Регистрирует telemost, wbstream, jitsi, none в реестре carrier через `registerEngineAuth` (связывает auth provider с engine) и `registerDirect` (прямое подключение без auth) |
| `builtin/engine_adapter.go` | Адаптер `engine.Session``carrier.Session`. Связывает auth provider (Issue → Credentials) с engine (Connect с URL+Token). Поддерживает Refresh callback для engines, требующих свежие credentials при реконнекте (Goolom) |
### `internal/engine/`
@@ -315,7 +314,7 @@ internal/e2e/ E2E тесты на реальных провайдер
| `engine.go` | Интерфейс `Session` (Connect, Send, Close, WatchConnection, CanSend и т.д.) + `Factory` + реестр. `Config` содержит URL, Token, Extra, OnData, DNSServer, Refresh callback. `Capabilities`: ByteStream, VideoTrack |
| `livekit/engine.go` | LiveKit engine — используется WB Stream. Подключается через LiveKit SDK, публикует/подписывается на DataChannel и VideoTrack |
| `goolom/engine.go` | Goolom engine — проприетарный протокол Яндекса (Telemost). WebSocket signaling, dual pub/sub PeerConnections, DataChannel, telemetry. Использует `Refresh` callback для получения свежих credentials при реконнекте |
| `salutejazz/engine.go` | SaluteJazz engine — протокол Сбера. WebSocket + SDP signaling, pub/sub split, `_reliable` DataChannel, length-prefixed DataPacket envelope |
| `jitsi/engine.go` | Jitsi engine — MUC/Jingle/colibri-ws, byte stream через bridge channel и best-effort VideoTrack |
### `internal/auth/`
@@ -324,7 +323,7 @@ internal/e2e/ E2E тесты на реальных провайдер
| `auth.go` | Интерфейс `Provider` (Engine, DefaultServiceURL, Issue) + `RoomCreator` + реестр. `Credentials`: URL, Token, Extra |
| `wbstream/provider.go` | WB Stream auth: guest register → join room → token exchange. Реализует `RoomCreator`. `Engine()``"livekit"`, `DefaultServiceURL()``"https://stream.wb.ru"` |
| `telemost/provider.go` | Yandex Telemost auth: HTTP connection-info → engine credentials. `Engine()``"goolom"`, `DefaultServiceURL()``"https://telemost.yandex.ru"` |
| `salutejazz/provider.go` | SaluteJazz auth: create-meeting + preconnect flow. Реализует `RoomCreator`. `Engine()``"salutejazz"`. Принимает room в формате `<roomID>:<password>` |
| `jitsi/provider.go` | Jitsi auth: разбирает URL комнаты и передаёт параметры engine. `Engine()``"jitsi"` |
### `internal/crypto/`
@@ -383,8 +382,6 @@ internal/e2e/ E2E тесты на реальных провайдер
| `telemost_poc_datachannel.py` | Базовый PoC: два гостя в одной Telemost комнате, обмен данными через DataChannel |
| `telemost_poc_videochannel.py` | Передача данных QR-кодами в видеопотоке Telemost |
| `telemost_info.py` | Сбор полной информации о Telemost конференции: участники, кодеки, ICE серверы, SDP |
| `jazz_poc_datachannel.py` | PoC DataChannel через SaluteJazz |
| `jazz_info.py` | Информация о Jazz конференции |
| `wbstream_poc_datachannel.py` | PoC DataChannel через WB Stream |
| `wbstream_poc_videochannel.py` | PoC видеоканала через WB Stream |
| `wbstream_info.py` | Информация о WB Stream комнате |
@@ -426,16 +423,7 @@ internal/e2e/ E2E тесты на реальных провайдер
## 6. Carriers - провайдеры
Carrier - это WebRTC сервис видеозвонков, через который идёт туннель. Все три в белых списках у российских провайдеров.
### SaluteJazz (`jazz`)
- Сервис видеозвонков от Сбера: `salutejazz.ru`
- Не требует регистрации для участника (только организатор)
- DataChannel работает, но Jazz **банит IP** за паттерны трафика характерные для DataChannel туннеля
- VideoTrack **не работает** для туннелирования (все non-data транспорты fail в E2E тестах)
- Поддерживает автогенерацию Room ID (`mode: gen`)
- Инициализация звонка изнутри автоматически реализована
Carrier - это WebRTC сервис видеозвонков, через который идёт туннель.
### Yandex Telemost (`telemost`)
@@ -467,7 +455,7 @@ Transport определяет как именно данные упаковыв
- Лимит payload: 12KB на сообщение (ограничение SFU)
- Надёжный, упорядоченный (SCTP гарантирует)
- Работает только с jazz (но Jazz банит IP за паттерны трафика)
- Работает с Jitsi и direct engine-сценариями
- Telemost удалил DataChannel
- WB Stream DataChannel **не работает** в обычном guest flow — токены выдаются с `canPublishData=false`
@@ -476,7 +464,6 @@ Transport определяет как именно данные упаковыв
Данные упаковываются в VP8 видеофреймы. Поверх этого строится KCP - надёжный протокол с повторной передачей, работающий поверх ненадёжного канала.
- Работает с telemost и wbstream (pass в E2E тестах)
- Jazz не поддерживает VideoTrack для туннелирования (fail)
- Большой пинг из-за батчинга фреймов
- KCP параметры: MTU 1400, окно 4096, conv ID `0xC0FFEE01`
- Рекомендуется: `vp8.fps: 60`, `vp8.batch_size: 64`
@@ -489,7 +476,7 @@ Transport определяет как именно данные упаковыв
- UUID для SEI payload: `5dc03ba8-450f-4b55-9a77-1f916c5b0739`
- ACK timeout (по умолчанию 3с), фрагментация, ретрансмиссия до 4 попыток
- Работает только с wbstream (pass в E2E тестах)
- Telemost и Jazz не поддерживают (fail)
- Telemost не поддерживает (fail)
- Рекомендуется: `sei.fps: 60`, `sei.batch_size: 64`, `sei.fragment_size: 900`, `sei.ack_timeout_ms: 2000`
### videochannel
@@ -500,7 +487,7 @@ Transport определяет как именно данные упаковыв
**tile** - тайловый кодек, только 1080x1080. Пиксели кодируют биты напрямую. Reed-Solomon коррекция ошибок. Параметры: размер тайла в пикселях (1..270), процент избыточности (0..200). Быстрее QR но нестабильнее.
Общее: ffmpeg как subprocess, поддержка NVENC, VP8 видеопоток. Самый медленный транспорт. Работает стабильно с wbstream, best effort с telemost, не работает с jazz.
Общее: ffmpeg как subprocess, поддержка NVENC, VP8 видеопоток. Самый медленный транспорт. Работает стабильно с wbstream, best effort с telemost.
---
@@ -583,10 +570,6 @@ Community Android клиент: [alananisimov/olcbox](https://github.com/alanani
- `telemost_poc_videochannel.py` - QR в видео, `vcsend.py` - передача файлов
- `telemost_info.py` - полный дамп SDP, ICE серверов, участников
**Jazz:**
- `jazz_poc_datachannel.py` - DataChannel через Jazz SFU
- `jazz_info.py` - информация о конференции
**WB Stream:**
- `wbstream_poc_datachannel.py` - DataChannel
- `wbstream_poc_videochannel.py` - видеоканал
@@ -713,7 +696,7 @@ olcrtc config.yaml
|---|---|
| `mode` | `srv` - сервер, `cnc` - клиент, `gen` - генерация Room ID |
| `link` | Всегда `direct` |
| `auth.provider` | `telemost`, `jazz`, `wbstream` или `none` |
| `auth.provider` | `telemost`, `wbstream`, `jitsi` или `none` |
| `room.id` | Room ID |
| `crypto.key` | Ключ шифрования hex 64 символа |
| `net.transport` | `datachannel`, `vp8channel`, `seichannel`, `videochannel` |
@@ -790,19 +773,17 @@ olcrtc://wbstream?vp8channel<vp8-fps=60&vp8-batch=64>@room-01#key$RU / free
## 16. Матрица совместимости
| Transport | telemost | jazz | wbstream |
| Transport | telemost | wbstream | jitsi |
|---|:---:|:---:|:---:|
| datachannel | `-` | `+` | `-` |
| vp8channel | `+` | `-` | `+` |
| seichannel | `-` | `-` | `+` |
| videochannel | `~` | `-` | `+` |
| datachannel | `-` | `-` | `+` |
| vp8channel | `+` | `+` | `~` |
| seichannel | `-` | `+` | `~` |
| videochannel | `~` | `+` | `~` |
- `+` работает (pass в E2E тестах)
- `-` не работает / не поддерживается (fail в E2E тестах)
- `~` best effort (может работать, но нестабильно)
**Jazz:** только datachannel проходит E2E тесты. Все non-data транспорты (vp8channel, seichannel, videochannel) помечены как expected fail — Jazz не поддерживает VideoTrack для туннелирования. Кроме того, Jazz **банит IP** за паттерны datachannel трафика.
**Telemost:** только vp8channel стабильно проходит. DataChannel удалён из Telemost. seichannel не поддерживается. videochannel — best effort.
**WBStream:** все транспорты кроме datachannel работают. DataChannel помечен как expected fail — в обычном guest flow WB Stream выдаёт токены с `canPublishData=false`, и DC не маршрутизирует данные. Для DC нужны модераторские/permission права.
@@ -858,14 +839,6 @@ WB Stream - текущий приоритет. Основа уже реализ
- [ ] Авто перезапуск звонка
- [ ] TLS стек Chrome
**Issue #1 - реализовать поддержку salutejazz.ru** `enhancement`
- [ ] Симуляция XHR телеметрии
- [ ] Симуляция задержек
- [ ] Система завершения звонка
- [ ] Авто перезапуск звонка
- [ ] TLS стек Chrome
### Закрытые (уже сделано)
| Issue | Что было |
@@ -899,7 +872,6 @@ WB Stream - текущий приоритет. Основа уже реализ
| **Kot-nikot** | 3 | Фиксы |
| **HLNikNiky** / Sesdear | 2 | URI добавление, фиксы |
| **Denis Suchok** / DeNcHiK3713 | 1 | Windows Podman скрипты |
| **0xcodepunk** | 1 | SaluteJazz PoC DataChannel (issue #10) |
| **scalebb2** | 1 | - |
---

View File

@@ -19,7 +19,7 @@ olcrtc /etc/olcrtc/server.yaml
|------------------------------------------------------------------|-----------------------------------------------------------|
| `mode` | `srv`, `cnc`, or `gen` |
| `link` | `direct` |
| `auth.provider` | `jitsi`, `telemost`, `jazz`, `wbstream`, `none` |
| `auth.provider` | `jitsi`, `telemost`, `wbstream`, `none` |
| `room.id` | conference room id |
| `crypto.key` / `crypto.key_file` | 64-char hex (32 bytes), inline or read from file |
| `net.transport` | `datachannel`, `videochannel`, `seichannel`, `vp8channel` |

View File

@@ -96,9 +96,8 @@ cd olcrtc
Select auth provider:
1) jitsi
2) telemost
3) jazz
4) wbstream
Enter choice [1-4, default: 1]:
3) wbstream
Enter choice [1-3, default: 1]:
```
Выбери сервис. Полную матрицу совместимости смотри в [settings.md](settings.md).
@@ -117,7 +116,7 @@ Enter choice [1-4, default: 1]:
```
Рекомендации:
- **datachannel** - самый быстрый, минимальный пинг. Стабильно работает с `jitsi` через colibri-ws bridge channel. С `jazz` тоже работает, но Jazz банит IP за паттерны трафика. **WBStream DC не работает** в обычном guest flow (токены без `canPublishData`). **Telemost удалил DC**.
- **datachannel** - самый быстрый, минимальный пинг. Стабильно работает с `jitsi` через colibri-ws bridge channel. **WBStream DC не работает** в обычном guest flow (токены без `canPublishData`). **Telemost удалил DC**.
- **vp8channel** - работает с telemost и wbstream, быстрый, но большой пинг.
- **seichannel** - работает только с wbstream, медленный, но мелкий пинг.
- **videochannel** - работает с wbstream (стабильно) и telemost (best effort), самый медленный и большой пинг.
@@ -134,8 +133,6 @@ Enter Room ID:
Для **telemost** и **wbstream** - создай руму через сайт ([телемост](https://telemost.yandex.ru/), [wbstream](https://stream.wb.ru)) и вставь её ID.
Для **jazz** скрипт предложит выбор: сгенерировать автоматически (рекомендуется) или ввести существующий ID. При автогенерации скрипт запустит `gen` и получит ID до старта сервера. Также можно создать руму через сайт [jazz](https://salutejazz.ru/calls/create).
### DNS
```

View File

@@ -65,7 +65,7 @@ Important fields:
| YAML | Runtime field | Notes |
|---|---|---|
| `mode` | `session.Config.Mode` | `srv`, `cnc`, or `gen`. |
| `auth.provider` | `Auth` | `jitsi`, `telemost`, `jazz`, `wbstream`, or `none`. |
| `auth.provider` | `Auth` | `jitsi`, `telemost`, `wbstream`, or `none`. |
| `room.id` | `RoomID` | Carrier-specific room reference. |
| `crypto.key` / `crypto.key_file` | `KeyHex` | Shared 32-byte key encoded as 64 hex chars. |
| `net.transport` | `Transport` | `datachannel`, `vp8channel`, `seichannel`, or `videochannel`. |
@@ -187,7 +187,6 @@ The universal-carrier refactor centers on small registries:
```text
carrier "wbstream" -> auth/wbstream -> engine/livekit
carrier "jazz" -> auth/salutejazz -> engine/salutejazz
carrier "telemost"-> auth/telemost -> engine/goolom
carrier "jitsi" -> auth/jitsi -> engine/jitsi
carrier "none" -> direct user-supplied engine/url/token
@@ -200,7 +199,6 @@ carrier "none" -> direct user-supplied engine/url/token
| `jitsi` | `jitsi` | No | Parses host/room from a public or self-hosted Jitsi URL. No HTTP auth. |
| `telemost` | `goolom` | No | Calls Telemost room-info flow and returns Goolom credentials. |
| `wbstream` | `livekit` | Yes | Registers guest, optionally creates room, joins room, fetches LiveKit token. |
| `jazz` / `salutejazz` | `salutejazz` | Yes | Creates or joins SaluteJazz room and returns room/password tuple. |
| `none` | chosen by config | No | Direct engine mode for downstream tools or self-hosted SFUs. |
## Engines
@@ -212,7 +210,6 @@ Engines expose the low-level service/SFU protocol.
| `livekit` | `internal/engine/livekit` | Yes | Yes | LiveKit SDK room, data packets, local/remote tracks, reconnect with credential refresh. |
| `goolom` | `internal/engine/goolom` | Yes | Yes | Yandex Telemost/Goolom signaling, split publisher/subscriber peer connections, telemetry/keepalive. |
| `jitsi` | `internal/engine/jitsi` | Yes | Best effort | Jitsi MUC/Jingle/colibri-ws plus optional video track negotiation. |
| `salutejazz` | `internal/engine/salutejazz` | Yes | Yes | SaluteJazz WebSocket signaling and split media peer connections. |
Engine work is where most provider breakage and reconnect complexity lives.
@@ -223,7 +220,7 @@ either a byte stream or a video track.
| Transport | Primitive | Reliability model | Best fit | Notes |
|---|---|---|---|---|
| `datachannel` | Carrier byte stream | Native reliable ordered messages | Jitsi, direct engines, some Jazz cases | Simple pass-through with 12 KiB message cap. |
| `datachannel` | Carrier byte stream | Native reliable ordered messages | Jitsi and direct engines | Simple pass-through with 12 KiB message cap. |
| `vp8channel` | VP8 video track | KCP over VP8-looking frames | WB Stream and Telemost-style video paths | Highest-performance video-path transport. Uses epochs and binding tokens to survive restarts/loopback. |
| `seichannel` | H264 SEI video track | Custom fragments + ACK/retry | WB Stream fallback | Carries data in SEI NAL units with fragmentation, CRC, ACK. |
| `videochannel` | Visual frames via ffmpeg | QR/tile frames + ACK/retry | Experimental/inspection-friendly path | Encodes visual payload frames, requires ffmpeg, supports QR and tile codecs. |

View File

@@ -7,10 +7,10 @@ mode: srv
link: direct # p2p link type
auth:
provider: jitsi # jitsi | telemost | jazz | wbstream | none
provider: jitsi # jitsi | telemost | wbstream | none
# For jitsi: full conference URL (https://host/room or host/room).
# For telemost / wbstream / jazz: room ID returned by the service.
# For telemost / wbstream: room ID returned by the service.
room:
id: "https://meet.small-dm.ru/REPLACE_WITH_ROOM_NAME"
@@ -45,7 +45,7 @@ socks:
# Direct engine mode — only used when auth.provider is "none"
engine:
name: "" # livekit | goolom | salutejazz | jitsi
name: "" # livekit | goolom | jitsi
url: ""
token: ""

View File

@@ -12,20 +12,18 @@
## Матрица совместимости
| Transport | telemost | jazz | wbstream | jitsi |
|-----------|:--------:|:----:|:--------:|:-----:|
| datachannel | - | ~ | ~ | + |
| vp8channel | + | - | + | ~ |
| seichannel | - | - | + | ~ |
| videochannel | + | - | + | ~ |
| Transport | telemost | wbstream | jitsi |
|-----------|:--------:|:--------:|:-----:|
| datachannel | - | ~ | + |
| vp8channel | + | + | ~ |
| seichannel | - | + | ~ |
| videochannel | + | + | ~ |
**Легенда:**
- `+` - работает (pass в E2E тестах)
- `-` - не работает / не поддерживается (fail в E2E тестах)
- `~` - нестабильно (может работать, но нестабильно)
**Jazz:** только datachannel проходит E2E тесты. Все non-data транспорты (vp8channel, seichannel, videochannel) не работают — Jazz не поддерживает VideoTrack для туннелирования. Кроме того, Jazz **банит IP** за паттерны datachannel трафика.
**Telemost:** только vp8channel стабильно проходит. DataChannel удалён из Telemost. seichannel не поддерживается. videochannel — best effort.
**WBStream:** все транспорты кроме datachannel работают. DataChannel в обычном guest flow без выдавания модератора не работает — WB Stream выдаёт токены с `canPublishData=false`, и DC не маршрутизирует данные.
@@ -45,7 +43,7 @@
| YAML поле | Что вводить |
|-----------|-------------|
| `mode` | `srv` на сервере, `cnc` на клиенте, `gen` для генерации Room ID |
| `auth.provider` | `telemost`, `jazz`, `wbstream` или `jitsi` |
| `auth.provider` | `telemost`, `wbstream` или `jitsi` |
| `net.transport` | `datachannel`, `vp8channel`, `seichannel` или `videochannel` |
| `room.id` | Room ID |
| `crypto.key` или `crypto.key_file` | Ключ шифрования hex 64 символа. Генерация: `openssl rand -hex 32` |
@@ -99,13 +97,13 @@ transport. Используй одинаковые traffic-настройки н
## mode: gen
Генерирует Room ID заранее, не запуская сервер. Поддерживается для auth-провайдеров с автосозданием комнат: `jazz` и `wbstream`. Для `telemost` комнату нужно создавать вручную через сайт.
Генерирует Room ID заранее, не запуская сервер. Поддерживается для auth-провайдеров с автосозданием комнат: `wbstream`. Для `telemost` комнату нужно создавать вручную через сайт.
**Обязательные поля:**
| YAML поле | Описание |
|-----------|----------|
| `auth.provider` | `jazz` или `wbstream` |
| `auth.provider` | `wbstream` |
| `net.dns` | DNS-сервер |
| `gen.amount` | Количество комнат |

View File

@@ -33,7 +33,7 @@ olcrtc://<Auth>?<Transport><key=value&key=value>@<RoomID>#<EncryptionKey>$<MIMO>
| Поле | Значение |
|------|----------|
| `<Auth>` | Имя auth-провайдера, например `telemost`, `jazz`, `wbstream`, `jitsi` |
| `<Auth>` | Имя auth-провайдера, например `telemost`, `wbstream`, `jitsi` |
| `<Transport>` | Имя транспорта, например `datachannel`, `vp8channel`, `seichannel`, `videochannel` |
| payload | Параметры транспорта в `<key=value&...>`. Ключи совпадают с YAML полями. Блок опускается если используются defaults |
| `<RoomID>` | Идентификатор комнаты или auth-specific room URL/ID |
@@ -162,10 +162,10 @@ vp8:
data: data
```
### jazz + seichannel
### wbstream + seichannel
```text
olcrtc://jazz?seichannel<fps=60&batch=64&frag=900&ack-ms=2000>@room-01#d823fa01cb3e0609b67322f7cf984c4ee2e4ce2e294936fc24ef38c9e59f4799$DE / olc free sub
olcrtc://wbstream?seichannel<fps=60&batch=64&frag=900&ack-ms=2000>@room-01#d823fa01cb3e0609b67322f7cf984c4ee2e4ce2e294936fc24ef38c9e59f4799$DE / olc free sub
```
### Эквивалент YAML
@@ -174,7 +174,7 @@ olcrtc://jazz?seichannel<fps=60&batch=64&frag=900&ack-ms=2000>@room-01#d823fa01c
mode: cnc
link: direct
auth:
provider: jazz
provider: wbstream
room:
id: "room-01"
crypto:

View File

@@ -29,7 +29,6 @@ const (
modeSRV = "srv"
modeCNC = "cnc"
modeGen = "gen"
authJazz = "jazz"
authNone = "none"
transportVideo = "videochannel"
transportVP8 = "vp8channel"
@@ -64,7 +63,7 @@ var (
ErrAmountRequired = errors.New("amount required for gen mode (set gen.amount)")
// ErrAuthRequired indicates that no auth provider was selected.
ErrAuthRequired = errors.New(
"auth provider required (set auth.provider to jitsi, telemost, jazz, wbstream or none)")
"auth provider required (set auth.provider to jitsi, telemost, wbstream or none)")
// ErrURLRequired indicates that auth.url must be provided when the auth provider has no default URL.
ErrURLRequired = errors.New("SFU URL required (set auth.url)")
// ErrUnsupportedCarrier indicates that carrier is not registered.
@@ -380,7 +379,7 @@ func validateTransportRegistration(cfg Config) error {
}
func validateCommon(cfg Config) error {
if cfg.RoomID == "" && cfg.Auth != authJazz && cfg.Auth != authNone {
if cfg.RoomID == "" && cfg.Auth != authNone {
return ErrRoomIDRequired
}
if cfg.KeyHex == "" {

View File

@@ -139,15 +139,6 @@ func TestValidate(t *testing.T) {
want error
}{
{name: "valid baseline", cfg: base},
{
name: "jazz allows empty room id",
cfg: func() Config {
cfg := base
cfg.Auth = "jazz"
cfg.RoomID = ""
return cfg
}(),
},
{
name: "cnc requires socks host and port",
cfg: func() Config {
@@ -186,7 +177,7 @@ func TestValidate(t *testing.T) {
want: ErrUnsupportedTransport,
},
{
name: "room id required for non jazz",
name: "room id required",
cfg: func() Config {
cfg := base
cfg.RoomID = ""
@@ -588,10 +579,6 @@ func TestValidateGen(t *testing.T) {
name: "valid wbstream",
cfg: Config{Auth: testAuthWBStream, DNSServer: "1.1.1.1:53", Amount: 3},
},
{
name: "valid jazz",
cfg: Config{Auth: "jazz", DNSServer: "1.1.1.1:53", Amount: 1},
},
{
name: "missing auth",
cfg: Config{DNSServer: "1.1.1.1:53", Amount: 1},

View File

@@ -1,7 +1,7 @@
// Package auth defines how room credentials are produced for an engine.
//
// An auth provider is responsible for any service-specific HTTP / login flow
// (WB Stream, SaluteJazz, Yandex Telemost, Jitsi, ...) and produces a
// (WB Stream, Yandex Telemost, Jitsi, ...) and produces a
// Credentials value that an engine can use to connect. Some auth providers
// also support creating new rooms; that capability is optional and is
// expressed via the RoomCreator interface.

View File

@@ -1,198 +0,0 @@
// Package salutejazz is the auth provider for the SaluteJazz service. It
// creates / joins a Jazz room over HTTP and returns the connector
// WebSocket URL, room ID and password that the salutejazz engine consumes.
package salutejazz
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/google/uuid"
"github.com/openlibrecommunity/olcrtc/internal/protect"
)
const (
authTypeAnonymous = "ANONYMOUS"
headerAccept = "Accept"
headerAuthType = "X-Jazz-AuthType"
headerClientID = "X-Jazz-ClientId"
headerClientType = "X-Client-AuthType"
headerContentType = "Content-Type"
headerJazzUA = "X-Jazz-Ua"
headerOrigin = "Origin"
headerReferer = "Referer"
contentTypeJSON = "application/json"
jazzOrigin = "https://salutejazz.ru"
jazzReferer = jazzOrigin + "/"
jazzUA = "osName=Linux;osVersion=;appName=jazz;appVersion=26.21.7;" +
"surface=WEB;browserName=Firefox;browserVersion=150.0"
)
var apiBase = "https://bk.salutejazz.ru" //nolint:gochecknoglobals // package-level state intentional
// roomInfo contains connection details for a SaluteJazz room.
type roomInfo struct {
RoomID string
Password string
ConnectorURL string
}
var (
errCreateRoomFailed = errors.New("create room failed")
errPreconnectFailed = errors.New("preconnect failed")
)
func anonymousHeaders() map[string]string {
return map[string]string{
headerAccept: "application/json, text/plain, */*",
headerAuthType: authTypeAnonymous,
headerClientID: uuid.New().String(),
headerClientType: authTypeAnonymous,
headerContentType: contentTypeJSON,
headerJazzUA: jazzUA,
headerOrigin: jazzOrigin,
headerReferer: jazzReferer,
}
}
func createRoom(ctx context.Context) (*roomInfo, error) {
headers := anonymousHeaders()
createResp, err := createMeeting(ctx, headers)
if err != nil {
return nil, fmt.Errorf("create meeting: %w", err)
}
connectorURL, err := preconnect(ctx, createResp.RoomID, createResp.Password, headers)
if err != nil {
return nil, fmt.Errorf("preconnect: %w", err)
}
return &roomInfo{
RoomID: createResp.RoomID,
Password: createResp.Password,
ConnectorURL: connectorURL,
}, nil
}
type createResponse struct {
RoomID string `json:"roomId"`
Password string `json:"password"`
}
func createMeeting(ctx context.Context, headers map[string]string) (*createResponse, error) {
createPayload := map[string]any{
"title": "Video meeting",
"guestEnabled": true,
"lobbyEnabled": false,
"serverVideoRecordAutoStartEnabled": false,
"sipEnabled": false,
"moderatorEmails": []string{},
"summarizationEnabled": false,
"room3dEnabled": false,
"room3dScene": "XRLobby",
}
body, err := json.Marshal(createPayload)
if err != nil {
return nil, fmt.Errorf("marshal create payload: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiBase+"/room/create-meeting",
bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
for k, v := range headers {
req.Header.Set(k, v)
}
client := protect.NewHTTPClient()
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("do create request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("create room status: %w", protect.StatusError(errCreateRoomFailed, resp, 1024))
}
var res createResponse
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, fmt.Errorf("decode create response: %w", err)
}
return &res, nil
}
func preconnect(ctx context.Context, roomID, password string, headers map[string]string) (string, error) {
preconnectPayload := map[string]any{
"password": password,
"jazzNextMigration": map[string]any{
"b2bBaseRoomSupport": true,
"demoRoomBaseSupport": true,
"demoRoomVersionSupport": 2,
"mediaWithoutAutoSubscribeSupport": true,
"webinarSpeakerSupport": true,
"webinarViewerSupport": true,
"sdkRoomSupport": true,
"sberclassRoomSupport": true,
},
}
preBody, err := json.Marshal(preconnectPayload)
if err != nil {
return "", fmt.Errorf("marshal preconnect payload: %w", err)
}
preReq, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
fmt.Sprintf("%s/room/%s/preconnect", apiBase, roomID),
bytes.NewReader(preBody),
)
if err != nil {
return "", fmt.Errorf("create preconnect request: %w", err)
}
for k, v := range headers {
preReq.Header.Set(k, v)
}
client := protect.NewHTTPClient()
preResp, err := client.Do(preReq)
if err != nil {
return "", fmt.Errorf("do preconnect request: %w", err)
}
defer func() { _ = preResp.Body.Close() }()
if preResp.StatusCode != http.StatusOK {
return "", fmt.Errorf("preconnect status: %w", protect.StatusError(errPreconnectFailed, preResp, 1024))
}
var preconnectResp struct {
ConnectorURL string `json:"connectorUrl"`
}
if err := json.NewDecoder(preResp.Body).Decode(&preconnectResp); err != nil {
return "", fmt.Errorf("decode preconnect response: %w", err)
}
return preconnectResp.ConnectorURL, nil
}
func joinRoom(ctx context.Context, roomID, password string) (*roomInfo, error) {
headers := anonymousHeaders()
connectorURL, err := preconnect(ctx, roomID, password, headers)
if err != nil {
return nil, err
}
return &roomInfo{
RoomID: roomID,
Password: password,
ConnectorURL: connectorURL,
}, nil
}

View File

@@ -1,143 +0,0 @@
package salutejazz
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/openlibrecommunity/olcrtc/internal/auth"
)
func withJazzAPIServer(t *testing.T, h http.Handler) {
t.Helper()
old := apiBase
srv := httptest.NewServer(h)
t.Cleanup(func() {
apiBase = old
srv.Close()
})
apiBase = srv.URL
}
func TestCreateMeetingAndPreconnect(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("POST /room/create-meeting", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Jazz-Authtype") != authTypeAnonymous {
t.Fatalf("missing auth header: %v", r.Header)
}
_ = json.NewEncoder(w).Encode(createResponse{RoomID: "room-1", Password: "pass"}) //nolint:gosec
})
mux.HandleFunc("POST /room/room-1/preconnect", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]string{connectorURLKey: testConnector})
})
withJazzAPIServer(t, mux)
headers := map[string]string{
headerAuthType: authTypeAnonymous,
"Content-Type": "application/json",
}
created, err := createMeeting(context.Background(), headers)
if err != nil {
t.Fatalf("createMeeting() error = %v", err)
}
if created.RoomID != "room-1" || created.Password != "pass" {
t.Fatalf("createMeeting() = %+v", created)
}
connector, err := preconnect(context.Background(), "room-1", "pass", headers)
if err != nil {
t.Fatalf("preconnect() error = %v", err)
}
if connector != testConnector {
t.Fatalf("preconnect() = %q", connector)
}
}
const (
testRoomID = "new-room"
testPassword = "new-pass"
testConnector = "wss://connector"
connectorURLKey = "connectorUrl"
)
func TestCreateRoomAndJoinRoom(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("POST /room/create-meeting", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(createResponse{RoomID: testRoomID, Password: testPassword}) //nolint:gosec
})
mux.HandleFunc("POST /room/{id}/preconnect", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]string{connectorURLKey: testConnector})
})
withJazzAPIServer(t, mux)
room, err := createRoom(context.Background())
if err != nil {
t.Fatalf("createRoom() error = %v", err)
}
if room.RoomID != testRoomID || room.Password != testPassword ||
room.ConnectorURL != testConnector {
t.Fatalf("createRoom() = %+v", room)
}
room, err = joinRoom(context.Background(), "existing", "secret")
if err != nil {
t.Fatalf("joinRoom() error = %v", err)
}
if room.RoomID != "existing" || room.Password != "secret" || room.ConnectorURL != testConnector {
t.Fatalf("joinRoom() = %+v", room)
}
}
func TestJazzAPIErrors(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/room/create-meeting", func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "bad", http.StatusTeapot)
})
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "bad", http.StatusInternalServerError)
})
withJazzAPIServer(t, mux)
if _, err := createMeeting(context.Background(), nil); !errors.Is(err, errCreateRoomFailed) {
t.Fatalf("createMeeting() error = %v, want %v", err, errCreateRoomFailed)
}
if _, err := preconnect(context.Background(), "room", "pass", nil); !errors.Is(err, errPreconnectFailed) {
t.Fatalf("preconnect() error = %v, want %v", err, errPreconnectFailed)
}
}
func TestJazzIssue(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("POST /room/create-meeting", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(createResponse{RoomID: testRoomID, Password: testPassword}) //nolint:gosec
})
mux.HandleFunc("POST /room/{id}/preconnect", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]string{connectorURLKey: testConnector})
})
withJazzAPIServer(t, mux)
p := Provider{}
creds, err := p.Issue(context.Background(), auth.Config{
RoomURL: "any",
Name: "peer",
})
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
if creds.URL != testConnector {
t.Fatalf("creds.URL = %q", creds.URL)
}
if creds.Token != testRoomID {
t.Fatalf("creds.Token = %q", creds.Token)
}
if creds.Extra["password"] != testPassword {
t.Fatalf("creds.Extra[password] = %q", creds.Extra["password"])
}
}

View File

@@ -1,70 +0,0 @@
package salutejazz
import (
"context"
"fmt"
"strings"
"github.com/openlibrecommunity/olcrtc/internal/auth"
)
// Provider produces SaluteJazz credentials.
type Provider struct{}
// Engine reports which engine consumes credentials from this auth provider.
func (Provider) Engine() string { return "salutejazz" }
// DefaultServiceURL returns the SaluteJazz service URL.
func (Provider) DefaultServiceURL() string { return "https://bk.salutejazz.ru" }
// Issue runs the SaluteJazz API flow and returns engine credentials.
//
// cfg.RoomURL accepts either an empty value (a new room is created on the
// fly, mirroring the legacy jazz provider) or "<roomID>:<password>".
func (Provider) Issue(ctx context.Context, cfg auth.Config) (auth.Credentials, error) {
roomRef := strings.TrimSpace(cfg.RoomURL)
var info *roomInfo
var err error
switch roomRef {
case "", "any", "dummy":
info, err = createRoom(ctx)
if err != nil {
return auth.Credentials{}, fmt.Errorf("create room: %w", err)
}
default:
roomID, password, hasPassword := strings.Cut(roomRef, ":")
if !hasPassword {
return auth.Credentials{}, fmt.Errorf("%w: expected <roomID>:<password>", auth.ErrRoomIDRequired)
}
info, err = joinRoom(ctx, roomID, password)
if err != nil {
return auth.Credentials{}, fmt.Errorf("join room: %w", err)
}
}
return auth.Credentials{
URL: info.ConnectorURL,
Token: info.RoomID,
Extra: map[string]string{
"password": info.Password,
"roomID": info.RoomID,
},
}, nil
}
// CreateRoom creates a new SaluteJazz room and returns "<roomID>:<password>".
//
// Returned format mirrors the legacy gen-mode output so existing
// subscriptions and tooling keep working.
func (Provider) CreateRoom(ctx context.Context, _ auth.Config) (string, error) {
info, err := createRoom(ctx)
if err != nil {
return "", fmt.Errorf("create room: %w", err)
}
return info.RoomID + ":" + info.Password, nil
}
func init() { //nolint:gochecknoinits // auth registration is the canonical Go pattern for plugins
auth.Register("salutejazz", Provider{})
}

View File

@@ -79,7 +79,7 @@ type Failover struct {
// Auth selects the auth provider.
type Auth struct {
Provider string `yaml:"provider"` // telemost, jazz, wbstream, none
Provider string `yaml:"provider"` // telemost, wbstream, none
}
// Room identifies the conference room.
@@ -112,7 +112,7 @@ type SOCKS struct {
// Engine selects a direct SFU connection when Auth.Provider is "none".
type Engine struct {
Name string `yaml:"name"` // livekit, goolom, salutejazz
Name string `yaml:"name"` // livekit, goolom, jitsi
URL string `yaml:"url"`
Token string `yaml:"token"`
}

View File

@@ -21,7 +21,6 @@ import (
"github.com/openlibrecommunity/olcrtc/internal/app/session"
"github.com/openlibrecommunity/olcrtc/internal/auth"
authSaluteJazz "github.com/openlibrecommunity/olcrtc/internal/auth/salutejazz"
authWBStream "github.com/openlibrecommunity/olcrtc/internal/auth/wbstream"
"github.com/openlibrecommunity/olcrtc/internal/client"
"github.com/openlibrecommunity/olcrtc/internal/engine"
@@ -74,11 +73,6 @@ var (
"datachannel,videochannel,seichannel,vp8channel",
"comma-separated transports for real e2e",
)
realE2EJazzRoom = flag.String( //nolint:gochecknoglobals // package-level state intentional
"olcrtc.real-jazz-room",
"",
"SaluteJazz room for real e2e, format roomID:password; autogenerated when empty",
)
realE2ETelemostRoom = flag.String( //nolint:gochecknoglobals // package-level state intentional
"olcrtc.real-telemost-room",
"41514917109506",
@@ -368,7 +362,7 @@ func registerFailingCarrier(t *testing.T) string {
}
func builtInCarrierNames() []string {
return []string{"jazz", "telemost", "wbstream", "jitsi"} //nolint:goconst // test literal, repetition is intentional
return []string{"telemost", "wbstream", "jitsi"} //nolint:goconst // test literal, repetition is intentional
}
func builtInTransportNames() []string {
@@ -392,11 +386,6 @@ func realE2ECaseExpectation(carrierName, transportName string) realE2EExpectatio
return realE2EExpectFail
}
return realE2EExpectPass
case "jazz":
if transportName == transportData {
return realE2EExpectPass
}
return realE2EExpectFail
case "jitsi":
// Jitsi colibri-ws bridge channel maps cleanly onto the
// datachannel transport (raw bytes broadcast through
@@ -453,30 +442,6 @@ func TestRealE2ECaseExpectation(t *testing.T) {
transport string
want realE2EExpectation
}{
{
name: "jazz datachannel is expected to pass",
carrier: "jazz",
transport: transportData,
want: realE2EExpectPass,
},
{
name: "jazz videochannel is expected to fail",
carrier: "jazz",
transport: transportVideo,
want: realE2EExpectFail,
},
{
name: "jazz seichannel is expected to fail",
carrier: "jazz",
transport: transportSEI,
want: realE2EExpectFail,
},
{
name: "jazz vp8channel is expected to fail",
carrier: "jazz",
transport: transportVP8,
want: realE2EExpectFail,
},
{
name: "telemost datachannel is expected to fail",
carrier: "telemost",
@@ -547,15 +512,6 @@ func realRoomURL(ctx context.Context, t *testing.T, carrierName string) string {
t.Helper()
switch carrierName {
case "jazz":
if *realE2EJazzRoom != "" {
return *realE2EJazzRoom
}
room, err := authSaluteJazz.Provider{}.CreateRoom(ctx, auth.Config{Name: "olcrtc-e2e-room"})
if err != nil {
t.Skipf("skip jazz real e2e: create room failed: %v", err)
}
return room
case "telemost":
room := *realE2ETelemostRoom
if room != "" && !strings.HasPrefix(room, "http://") && !strings.HasPrefix(room, "https://") {

View File

@@ -13,14 +13,12 @@ import (
"github.com/openlibrecommunity/olcrtc/internal/auth"
authJitsi "github.com/openlibrecommunity/olcrtc/internal/auth/jitsi"
authSaluteJazz "github.com/openlibrecommunity/olcrtc/internal/auth/salutejazz"
authTelemost "github.com/openlibrecommunity/olcrtc/internal/auth/telemost"
authWBStream "github.com/openlibrecommunity/olcrtc/internal/auth/wbstream"
"github.com/openlibrecommunity/olcrtc/internal/engine"
_ "github.com/openlibrecommunity/olcrtc/internal/engine/goolom" // register goolom engine via init
_ "github.com/openlibrecommunity/olcrtc/internal/engine/jitsi" // register jitsi engine via init
_ "github.com/openlibrecommunity/olcrtc/internal/engine/livekit" // register livekit engine via init
_ "github.com/openlibrecommunity/olcrtc/internal/engine/salutejazz" // register salutejazz engine via init
_ "github.com/openlibrecommunity/olcrtc/internal/engine/goolom" // register goolom engine via init
_ "github.com/openlibrecommunity/olcrtc/internal/engine/jitsi" // register jitsi engine via init
_ "github.com/openlibrecommunity/olcrtc/internal/engine/livekit" // register livekit engine via init
)
// ErrCarrierNotFound is returned when an unregistered carrier name is requested.
@@ -75,11 +73,10 @@ func Available() []string {
return names
}
// RegisterDefaults wires the built-in carriers: jitsi, telemost, jazz, wbstream
// RegisterDefaults wires the built-in carriers: jitsi, telemost, wbstream
// and "none" (direct engine access).
func RegisterDefaults() {
registerEngineAuth("wbstream", authWBStream.Provider{})
registerEngineAuth("jazz", authSaluteJazz.Provider{})
registerEngineAuth("telemost", authTelemost.Provider{})
registerEngineAuth("jitsi", authJitsi.Provider{})
registerDirect("none")

View File

@@ -4,7 +4,7 @@
// byte/video primitives the rest of olcrtc consumes.
//
// Engines model the SFU protocol family (e.g. LiveKit, Goolom). Service-
// specific bits (e.g. WB / Jazz / Telemost API flows) live in the auth
// specific bits (e.g. WB / Telemost API flows) live in the auth
// package, not here.
package engine
@@ -41,7 +41,7 @@ type Credentials struct {
// Config is the runtime input to an engine factory. URL/Token are produced by
// an auth provider (or supplied directly by the caller for "none" auth).
// Extra carries engine-specific fields that don't fit the common shape
// (e.g. SaluteJazz needs a separate room password alongside the room ID).
// (e.g. providers that need metadata beyond URL/token can pass it here).
//
// Refresh, when set, is called by an engine whose protocol requires fresh
// credentials on each reconnect (e.g. Goolom: every reconnect needs a new

View File

@@ -12,7 +12,7 @@
//
// The Jingle session-initiate is only delivered by Jicofo once at least one
// other participant is present in the conference, mirroring the Telemost /
// SaluteJazz two-peer requirement that olcrtc already accommodates.
// two-peer tunnel model that olcrtc already accommodates.
package jitsi
import (

View File

@@ -3,7 +3,7 @@
//
// This engine is service-agnostic: it accepts a wss:// signaling URL and an
// access token, and provides byte-stream + video-track primitives over a
// LiveKit room. Service-specific token acquisition (e.g. WB Stream, Jazz,
// LiveKit room. Service-specific token acquisition (e.g. WB Stream,
// or a self-hosted LiveKit deployment) lives in the auth package.
package livekit

View File

@@ -1,164 +0,0 @@
package salutejazz
import (
"context"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/gorilla/websocket"
)
// TestCloseUnblocksHandleSignaling pins down the shutdown ordering: when a
// peer goroutine is parked in handleSignaling -> ws.ReadJSON, calling Close
// must close the WebSocket up front so ReadJSON returns immediately and the
// signaling loop exits within the closeWaitTimeout. The historical bug had
// Close call wg.Wait() BEFORE closing the WS, so handleSignaling stayed
// parked for the full timeout (and on flaky networks longer once pion's
// PeerConnection.Close kicked in too) — which on CI showed up as
// "tunnel goroutine did not stop: client" in the real e2e jazz matrix.
//
//nolint:cyclop // setup + handler + assertions naturally produces several branches in one test
func TestCloseUnblocksHandleSignaling(t *testing.T) {
upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
// Server side parks on a read so it never closes the connection
// from its end, forcing the client-side ReadJSON to depend on
// shutdownWebSocket flipping the read deadline / closing the conn.
serverDone := make(chan struct{})
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
t.Errorf("upgrade websocket: %v", err)
return
}
defer func() {
_ = conn.Close()
close(serverDone)
}()
_, _, _ = conn.ReadMessage()
}))
defer srv.Close()
wsURL := "ws" + srv.URL[len("http"):]
dialer := websocket.Dialer{HandshakeTimeout: 2 * time.Second}
conn, resp, err := dialer.Dial(wsURL, nil)
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
s := &Session{
ws: conn,
reconnectCh: make(chan struct{}, 1),
closeCh: make(chan struct{}),
sessionCloseCh: make(chan struct{}),
sendQueue: make(chan []byte, 1),
subscriberConn: make(chan struct{}),
publisherConn: make(chan struct{}),
videoNegotiated: make(chan struct{}),
}
// Mirror Connect's bookkeeping for the signaling goroutine so
// wg.Wait blocks on it during Close.
signalingDone := make(chan struct{})
s.wg.Add(1)
go func() {
defer s.wg.Done()
defer close(signalingDone)
s.handleSignaling(context.Background())
}()
start := time.Now()
if err := s.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
elapsed := time.Since(start)
// closeWaitTimeout is 2s; with the fix Close should return well under that
// because shutdownWebSocket trips ReadJSON's deadline up front. Allow some
// slack so this remains stable on slow CI runners but still fail loudly
// if the historical 2s wait creeps back in.
if elapsed > closeWaitTimeout-500*time.Millisecond {
t.Fatalf("Close() took %s, expected < %s; handleSignaling likely parked", elapsed, closeWaitTimeout)
}
select {
case <-signalingDone:
case <-time.After(time.Second):
t.Fatal("handleSignaling did not exit after Close")
}
// Drain the server side too so the test doesn't leak goroutines.
select {
case <-serverDone:
case <-time.After(time.Second):
}
}
// TestShutdownWebSocketIsIdempotent guards the contract that Close can be
// called more than once (e.g. by both the carrier teardown path and a
// defer in tests) without panicking. gorilla/websocket's Close returns
// ErrCloseSent on the second call which we tolerate.
func TestShutdownWebSocketIsIdempotent(t *testing.T) {
upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
t.Errorf("upgrade websocket: %v", err)
return
}
defer func() { _ = conn.Close() }()
_, _, _ = conn.ReadMessage()
}))
defer srv.Close()
wsURL := "ws" + srv.URL[len("http"):]
conn, resp, err := websocket.DefaultDialer.Dial(wsURL, nil)
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
s := &Session{ws: conn}
var wg sync.WaitGroup
wg.Add(2)
go func() { defer wg.Done(); s.shutdownWebSocket() }()
go func() { defer wg.Done(); s.shutdownWebSocket() }()
wg.Wait()
}
// TestCloseWithDeadlineDoesNotBlockOnStraggler pins down that a wedged
// PeerConnection.Close (modeled here as a never-returning closer) does not
// hold up Session.Close past its budget. The historical failure mode showed
// up in the real e2e matrix as "tunnel goroutine did not stop: client" when
// pion's TURN refresh storm kept the ICE agent alive long after the test
// asked it to shut down.
func TestCloseWithDeadlineDoesNotBlockOnStraggler(t *testing.T) {
deadline := 50 * time.Millisecond
block := make(chan struct{})
t.Cleanup(func() { close(block) })
closers := []func() error{
func() error { return nil },
func() error { <-block; return nil },
}
start := time.Now()
closeWithDeadline(closers, deadline)
elapsed := time.Since(start)
if elapsed > deadline*4 {
t.Fatalf("closeWithDeadline blocked for %s, expected ~%s", elapsed, deadline)
}
if elapsed < deadline {
t.Fatalf("closeWithDeadline returned in %s before deadline %s; straggler ignored",
elapsed, deadline)
}
}

View File

@@ -1,144 +0,0 @@
package salutejazz
import (
"encoding/binary"
"fmt"
"io"
"github.com/google/uuid"
)
func encodeVarint(value uint64) []byte {
buf := make([]byte, binary.MaxVarintLen64)
n := binary.PutUvarint(buf, value)
return buf[:n]
}
func encodeField(fieldNumber int, wireType int, data []byte) []byte {
tag := encodeVarint(uint64(fieldNumber)<<3 | uint64(wireType)) //nolint:gosec,lll // G115: bounded conversion verified by surrounding logic
switch wireType {
case 2:
length := encodeVarint(uint64(len(data)))
result := make([]byte, 0, len(tag)+len(length)+len(data))
result = append(result, tag...)
result = append(result, length...)
result = append(result, data...)
return result
default:
result := make([]byte, 0, len(tag)+len(data))
result = append(result, tag...)
result = append(result, data...)
return result
}
}
// EncodeDataPacket wraps a payload into a SaluteJazz data packet.
func EncodeDataPacket(payload []byte) []byte {
msgID := uuid.New().String()
userFields := encodeField(2, 2, payload)
userFields = append(userFields, encodeField(8, 2, []byte(msgID))...)
dp := encodeField(1, 0, encodeVarint(0))
dp = append(dp, encodeField(2, 2, userFields)...)
return dp
}
func readVarint(r io.ByteReader) (uint64, error) {
val, err := binary.ReadUvarint(r)
if err != nil {
return 0, fmt.Errorf("read uvarint: %w", err)
}
return val, nil
}
// DecodeDataPacket extracts the payload from a SaluteJazz data packet.
func DecodeDataPacket(raw []byte) ([]byte, bool) {
userData, ok := parseFields(raw, 2)
if !ok {
return nil, false
}
payload, ok := parseFields(userData, 2)
return payload, ok
}
func parseFields(data []byte, targetField int) ([]byte, bool) {
reader := &byteReader{data: data, pos: 0}
var result []byte
for reader.pos < len(reader.data) {
tagVal, err := readVarint(reader)
if err != nil {
break
}
fieldNumber := int(tagVal >> 3)
wireType := int(tagVal & 0x07)
fieldData, ok := handleWireType(reader, wireType, len(data))
if !ok {
return result, len(result) > 0
}
if fieldNumber == targetField && wireType == 2 {
result = fieldData
}
}
return result, len(result) > 0
}
func handleWireType(reader *byteReader, wireType int, dataLen int) ([]byte, bool) {
switch wireType {
case 0:
_, _ = readVarint(reader)
return nil, true
case 2:
length, err := readVarint(reader)
if err != nil {
return nil, false
}
if length > uint64(dataLen)-uint64(reader.pos) { //nolint:gosec,lll // G115: bounded conversion verified by surrounding logic
return nil, false
}
fieldData := make([]byte, length)
n, err := reader.Read(fieldData)
if err != nil || uint64(n) != length { //nolint:gosec // G115: bounded conversion verified by surrounding logic
return nil, false
}
return fieldData, true
case 1:
reader.pos += 8
return nil, true
case 5:
reader.pos += 4
return nil, true
default:
return nil, false
}
}
type byteReader struct {
data []byte
pos int
}
func (b *byteReader) ReadByte() (byte, error) {
if b.pos >= len(b.data) {
return 0, io.EOF
}
c := b.data[b.pos]
b.pos++
return c, nil
}
func (b *byteReader) Read(p []byte) (int, error) {
if b.pos >= len(b.data) {
return 0, io.EOF
}
n := copy(p, b.data[b.pos:])
b.pos += n
return n, nil
}

View File

@@ -1,70 +0,0 @@
package salutejazz
import (
"bytes"
"errors"
"io"
"testing"
)
func TestDataPacketRoundTrip(t *testing.T) {
payload := []byte("hello jazz")
raw := EncodeDataPacket(payload)
got, ok := DecodeDataPacket(raw)
if !ok {
t.Fatal("DecodeDataPacket() ok = false")
}
if !bytes.Equal(got, payload) {
t.Fatalf("DecodeDataPacket() = %q, want %q", got, payload)
}
}
func TestDecodeDataPacketRejectsMalformedPackets(t *testing.T) {
tests := [][]byte{
nil,
{0xff},
encodeField(1, 0, encodeVarint(0)),
{byte(2<<3 | 2), 10, 1},
{byte(3<<3 | 7), 0},
}
for _, raw := range tests {
if payload, ok := DecodeDataPacket(raw); ok {
t.Fatalf("DecodeDataPacket(%v) = (%q, true), want false", raw, payload)
}
}
}
func TestParseFieldsSkipsSupportedNonTargetWireTypes(t *testing.T) {
data := encodeField(1, 0, encodeVarint(150))
data = append(data, encodeField(3, 1, []byte("12345678"))...)
data = append(data, encodeField(4, 5, []byte("1234"))...)
data = append(data, encodeField(2, 2, []byte("target"))...)
got, ok := parseFields(data, 2)
if !ok || string(got) != "target" {
t.Fatalf("parseFields() = (%q, %v), want target", got, ok)
}
}
func TestByteReader(t *testing.T) {
r := &byteReader{data: []byte{1, 2, 3}}
b, err := r.ReadByte()
if err != nil || b != 1 {
t.Fatalf("ReadByte() = (%d, %v), want (1, nil)", b, err)
}
buf := make([]byte, 4)
n, err := r.Read(buf)
if err != nil || n != 2 || !bytes.Equal(buf[:n], []byte{2, 3}) {
t.Fatalf("Read() = (%d, %v, %v), want two bytes", n, err, buf[:n])
}
if _, err := r.ReadByte(); !errors.Is(err, io.EOF) {
t.Fatalf("ReadByte() error = %v, want EOF", err)
}
if n, err := r.Read(buf); !errors.Is(err, io.EOF) || n != 0 {
t.Fatalf("Read() = (%d, %v), want (0, EOF)", n, err)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,320 +0,0 @@
package salutejazz
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/websocket"
"github.com/pion/webrtc/v4"
)
const (
testJazzGroupID = "group-1"
testJazzRoomID = "room-1"
)
//nolint:cyclop // table-driven test naturally has many branches
func TestSessionStateHelpers(t *testing.T) {
s := &Session{
reconnectCh: make(chan struct{}, 1),
closeCh: make(chan struct{}),
sessionCloseCh: make(chan struct{}),
sendQueue: make(chan []byte, 1),
subscriberConn: make(chan struct{}),
publisherConn: make(chan struct{}),
}
s.resetMediaState()
if s.subscriberReady.Load() || s.publisherReady.Load() || s.subscriberConn == nil || s.publisherConn == nil {
t.Fatal("resetMediaState() did not reset readiness")
}
if s.hasLocalVideoTracks() {
t.Fatal("hasLocalVideoTracks() = true without tracks")
}
if err := s.AddVideoTrack(nil); err != nil {
t.Fatalf("AddVideoTrack(nil) error = %v", err)
}
if !s.hasLocalVideoTracks() {
t.Fatal("hasLocalVideoTracks() = false after AddVideoTrack")
}
s.SetVideoTrackHandler(func(*webrtc.TrackRemote, *webrtc.RTPReceiver) {})
if s.videoTrackHandler() == nil {
t.Fatal("videoTrackHandler() = nil")
}
cfg := defaultWebRTCConfig()
if cfg.SDPSemantics != webrtc.SDPSemanticsUnifiedPlan || cfg.BundlePolicy != webrtc.BundlePolicyMaxBundle {
t.Fatalf("defaultWebRTCConfig() = %+v", cfg)
}
if s.buildAPI() == nil {
t.Fatal("buildAPI() returned nil")
}
}
func TestSessionCallbacksQueueReconnectAndClose(t *testing.T) {
s := &Session{
reconnectCh: make(chan struct{}, 1),
closeCh: make(chan struct{}),
sessionCloseCh: make(chan struct{}),
sendQueue: make(chan []byte, 1),
}
s.SetReconnectCallback(func(*webrtc.DataChannel) {})
s.SetShouldReconnect(func() bool { return true })
s.SetEndedCallback(func(string) {})
if s.onReconnect == nil || s.shouldReconnect == nil || s.onEnded == nil {
t.Fatal("callbacks were not stored")
}
s.queueReconnect()
select {
case <-s.reconnectCh:
default:
t.Fatal("queueReconnect() did not enqueue")
}
s.SetShouldReconnect(func() bool { return false })
s.queueReconnect()
select {
case <-s.reconnectCh:
t.Fatal("queueReconnect() enqueued despite policy=false")
default:
}
done := make(chan struct{})
go func() {
s.WatchConnection(context.Background())
close(done)
}()
if err := s.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
<-done
if err := s.Send([]byte("closed")); !errors.Is(err, ErrDataChannelNotReady) {
t.Fatalf("Send() error = %v, want datachannel not ready", err)
}
}
func TestSessionCanSendVideoOnlyModes(t *testing.T) {
s := &Session{sendQueue: make(chan []byte, 1)}
s.subscriberReady.Store(true)
if !s.CanSend() {
t.Fatal("CanSend() = false for subscriber-ready session without local video")
}
_ = s.AddVideoTrack(nil)
if s.CanSend() {
t.Fatal("CanSend() = true with local video but publisher not ready")
}
s.publisherReady.Store(true)
if !s.CanSend() {
t.Fatal("CanSend() = false with subscriber and publisher ready")
}
s.closed.Store(true)
if s.CanSend() {
t.Fatal("CanSend() = true for closed session")
}
}
func TestSendPublisherTrackAddWritesJazzPayload(t *testing.T) {
msgCh := make(chan map[string]any, 1)
upgrader := websocket.Upgrader{
CheckOrigin: func(*http.Request) bool { return true },
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
t.Errorf("upgrade websocket: %v", err)
return
}
defer func() { _ = conn.Close() }()
var msg map[string]any
if err := conn.ReadJSON(&msg); err != nil {
t.Errorf("read json: %v", err)
return
}
msgCh <- msg
}))
defer server.Close()
wsURL := "ws" + server.URL[len("http"):]
conn, resp, err := websocket.DefaultDialer.Dial(wsURL, nil)
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
s := &Session{
roomID: testJazzRoomID,
groupID: testJazzGroupID,
ws: conn,
}
if err := s.sendPublisherTrackAdd("VIDEO", "CAMERA", false); err != nil {
t.Fatalf("sendPublisherTrackAdd() error = %v", err)
}
msg := <-msgCh
assertJazzTrackAddEnvelope(t, msg)
assertJazzTrackAddPayload(t, msg[keyPayload])
}
func TestHandleParticipantsUpdateUnmutesCameraTrack(t *testing.T) {
msgCh := make(chan map[string]any, 1)
upgrader := websocket.Upgrader{
CheckOrigin: func(*http.Request) bool { return true },
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
t.Errorf("upgrade websocket: %v", err)
return
}
defer func() { _ = conn.Close() }()
var msg map[string]any
if err := conn.ReadJSON(&msg); err != nil {
t.Errorf("read json: %v", err)
return
}
msgCh <- msg
}))
defer server.Close()
wsURL := "ws" + server.URL[len("http"):]
conn, resp, err := websocket.DefaultDialer.Dial(wsURL, nil)
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
s := &Session{
roomID: testJazzRoomID,
groupID: testJazzGroupID,
ws: conn,
videoTracks: []webrtc.TrackLocal{nil},
}
s.videoOffered.Store(true)
s.handleParticipantsUpdate(map[string]any{
"update": map[string]any{
"participants": []any{
map[string]any{
"isPublisher": true,
"tracks": []any{
map[string]any{
"sid": "TR_CAMERA_1",
"type": "VIDEO",
"source": "CAMERA",
payloadMuted: true,
},
},
},
},
},
})
msg := <-msgCh
assertJazzTrackAddEnvelope(t, msg)
assertJazzTrackMutedPayload(t, msg[keyPayload])
}
func TestJazzICECandidatePayload(t *testing.T) {
sdpMid := "0"
sdpMLineIndex := uint16(1)
usernameFragment := "ufrag-1"
got := jazzICECandidatePayload(webrtc.ICECandidateInit{
Candidate: "candidate:1 1 udp 1 127.0.0.1 12345 typ host",
SDPMid: &sdpMid,
SDPMLineIndex: &sdpMLineIndex,
UsernameFragment: &usernameFragment,
}, "PUBLISHER")
if got["candidate"] != "candidate:1 1 udp 1 127.0.0.1 12345 typ host" {
t.Fatalf("candidate = %v", got["candidate"])
}
if got["sdpMid"] != "0" {
t.Fatalf("sdpMid = %v, want 0", got["sdpMid"])
}
if got["sdpMLineIndex"] != uint16(1) {
t.Fatalf("sdpMLineIndex = %v, want 1", got["sdpMLineIndex"])
}
if got["usernameFragment"] != "ufrag-1" {
t.Fatalf("usernameFragment = %v, want ufrag-1", got["usernameFragment"])
}
if got["target"] != "PUBLISHER" {
t.Fatalf("target = %v, want PUBLISHER", got["target"])
}
}
func assertJazzTrackAddEnvelope(t *testing.T, msg map[string]any) {
t.Helper()
if msg[keyRoomID] != testJazzRoomID {
t.Fatalf("roomId = %v, want %s", msg[keyRoomID], testJazzRoomID)
}
if msg[keyEvent] != eventMediaIn {
t.Fatalf("event = %v, want %s", msg[keyEvent], eventMediaIn)
}
if msg[keyGroupID] != testJazzGroupID {
t.Fatalf("%s = %v, want %s", keyGroupID, msg[keyGroupID], testJazzGroupID)
}
}
func assertJazzTrackAddPayload(t *testing.T, raw any) {
t.Helper()
payload, ok := raw.(map[string]any)
if !ok {
t.Fatalf("payload missing or wrong type: %+v", raw)
}
if payload[payloadMethod] != "rtc:track:add" {
t.Fatalf("%s = %v, want rtc:track:add", payloadMethod, payload[payloadMethod])
}
track, ok := payload[payloadTrack].(map[string]any)
if !ok {
t.Fatalf("track missing or wrong type: %+v", payload[payloadTrack])
}
if track[payloadType] != "VIDEO" {
t.Fatalf("%s = %v, want VIDEO", payloadType, track[payloadType])
}
if track["source"] != "CAMERA" {
t.Fatalf("source = %v, want CAMERA", track["source"])
}
if track[payloadMuted] != false {
t.Fatalf("muted = %v, want false", track[payloadMuted])
}
}
func assertJazzTrackMutedPayload(t *testing.T, raw any) {
t.Helper()
payload, ok := raw.(map[string]any)
if !ok {
t.Fatalf("payload missing or wrong type: %+v", raw)
}
if payload[payloadMethod] != "rtc:track:muted" {
t.Fatalf("%s = %v, want rtc:track:muted", payloadMethod, payload[payloadMethod])
}
mute, ok := payload["mute"].(map[string]any)
if !ok {
t.Fatalf("mute missing or wrong type: %+v", payload["mute"])
}
if mute["sid"] != "TR_CAMERA_1" {
t.Fatalf("sid = %v, want TR_CAMERA_1", mute["sid"])
}
if mute[payloadMuted] != false {
t.Fatalf("muted = %v, want false", mute[payloadMuted])
}
}

View File

@@ -55,8 +55,6 @@ const (
defaultDNSServer = "1.1.1.1:53"
defaultHTTPPingURL = "https://www.google.com/generate_204"
carrierWBStream = "wbstream"
carrierJazz = "jazz"
roomURLAny = "any"
)
const (
@@ -165,7 +163,7 @@ func SetDebug(enabled bool) {
}
// Start launches the olcRTC client in background.
// carrierName: carrier name ("telemost", "jazz", "wbstream")
// carrierName: carrier name ("telemost", "wbstream", "jitsi")
// roomID: carrier-specific room ID
// clientID: client identifier that must match the server's -client-id
// keyHex: 64-char hex encryption key
@@ -746,7 +744,7 @@ func validateStartArgs(carrierName, roomID, clientID, keyHex string) error {
switch {
case carrierName == "":
return errCarrierRequired
case roomID == "" && carrierName != carrierJazz:
case roomID == "":
return errRoomIDRequired
case clientID == "":
return errClientIDRequired
@@ -761,11 +759,6 @@ func buildRoomURL(carrierName, roomID string) string {
switch carrierName {
case "telemost":
return "https://telemost.yandex.ru/j/" + roomID
case carrierJazz:
if roomID == "" {
return roomURLAny
}
return roomID
case carrierWBStream:
return roomID
default:

View File

@@ -122,16 +122,13 @@ func TestNormalizeBuildRoomAndClamp(t *testing.T) {
}
}
if normalizeCarrier(carrierWBStream) != carrierWBStream || normalizeCarrier("jazz") != "jazz" {
if normalizeCarrier(carrierWBStream) != carrierWBStream || normalizeCarrier("jitsi") != "jitsi" {
t.Fatal("normalizeCarrier() returned unexpected value")
}
if got := buildRoomURL("telemost", "abc"); got != "https://telemost.yandex.ru/j/abc" {
t.Fatalf("telemost room URL = %q", got)
}
if got := buildRoomURL("jazz", ""); got != "any" {
t.Fatalf("jazz empty room URL = %q", got)
}
if got := buildRoomURL(carrierWBStream, "room"); got != "room" {
t.Fatalf("wbstream room URL = %q", got)
}
@@ -150,17 +147,17 @@ func TestStartValidation(t *testing.T) {
if err := startWithConfig("telemost", dataTransport, "", "client", "key", 1080, "", "", mobileConfig{}); !errors.Is(err, errRoomIDRequired) { //nolint:lll // long test description
t.Fatalf("startWithConfig(missing room) = %v", err)
}
if err := startWithConfig("jazz", dataTransport, "", "", "key", 1080, "", "", mobileConfig{}); !errors.Is(err, errClientIDRequired) { //nolint:lll // long test description
if err := startWithConfig("jitsi", dataTransport, "room", "", "key", 1080, "", "", mobileConfig{}); !errors.Is(err, errClientIDRequired) { //nolint:lll // long test description
t.Fatalf("startWithConfig(missing client) = %v", err)
}
if err := startWithConfig("jazz", dataTransport, "", "client", "", 1080, "", "", mobileConfig{}); !errors.Is(err, errKeyHexRequired) { //nolint:lll // long test description
if err := startWithConfig("jitsi", dataTransport, "room", "client", "", 1080, "", "", mobileConfig{}); !errors.Is(err, errKeyHexRequired) { //nolint:lll // long test description
t.Fatalf("startWithConfig(missing key) = %v", err)
}
mu.Lock()
cancel = func() {}
mu.Unlock()
if err := startWithConfig("jazz", dataTransport, "", "client", "key", 1080, "", "", mobileConfig{}); !errors.Is(err, errAlreadyRunning) { //nolint:lll // long test description
if err := startWithConfig("jitsi", dataTransport, "room", "client", "key", 1080, "", "", mobileConfig{}); !errors.Is(err, errAlreadyRunning) { //nolint:lll // long test description
t.Fatalf("startWithConfig(running) = %v", err)
}
resetMobileGlobals(t)
@@ -176,8 +173,8 @@ func TestStartWithInjectedRunnerLifecycle(t *testing.T) {
runClientWithReady = func(ctx context.Context, cfg client.Config, onReady func()) error {
opts, _ := cfg.TransportOptions.(vp8channel.Options)
if cfg.Transport != dataTransport || cfg.Carrier != carrierJazz ||
cfg.RoomURL != "any" || cfg.DeviceID != "client" || cfg.LocalAddr != "127.0.0.1:1080" ||
if cfg.Transport != dataTransport || cfg.Carrier != "jitsi" ||
cfg.RoomURL != "room" || cfg.DeviceID != "client" || cfg.LocalAddr != "127.0.0.1:1080" ||
cfg.DNSServer != defaultDNSServer || opts.FPS != 60 || opts.BatchSize != 8 ||
cfg.Liveness.Interval != 2500*time.Millisecond ||
cfg.Liveness.Timeout != 750*time.Millisecond ||
@@ -194,7 +191,7 @@ func TestStartWithInjectedRunnerLifecycle(t *testing.T) {
return ctx.Err()
}
if err := StartWithTransport(carrierJazz, "dc", "", "client", "key", 1080, "", ""); err != nil {
if err := StartWithTransport("jitsi", "dc", "room", "client", "key", 1080, "", ""); err != nil {
t.Fatalf("StartWithTransport() error = %v", err)
}
if !IsRunning() {
@@ -252,7 +249,7 @@ func TestStartUsesDefaultsAndCheckWithInjectedRunner(t *testing.T) {
<-ctx.Done()
return nil
}
elapsed, err := Check("jazz", "dc", "", "client", "key", 1082, 100, -1, 999)
elapsed, err := Check("jitsi", "dc", "room", "client", "key", 1082, 100, -1, 999)
if err != nil {
t.Fatalf("Check() error = %v", err)
}
@@ -276,7 +273,7 @@ func TestPingPassesLiveness(t *testing.T) {
return nil
}
_, _ = Ping("jazz", "dc", "", "client", "key", 1085, 100, "http://127.0.0.1/", 30, 1)
_, _ = Ping("jitsi", "dc", "room", "client", "key", 1085, 100, "http://127.0.0.1/", 30, 1)
select {
case got := <-seen:
if got.Interval != 4000*time.Millisecond || got.Timeout != 1500*time.Millisecond || got.Failures != 6 {

View File

@@ -11,7 +11,7 @@
// conn, err := sess.Dial(ctx) // blocks until WebRTC data channel is ready
// // conn implements net.Conn — pass it to sing-box / any io.ReadWriter consumer
//
// Built-in auth providers (jitsi, telemost, jazz, wbstream):
// Built-in auth providers (jitsi, telemost, wbstream):
//
// sess, err := olcrtc.New(ctx, olcrtc.Config{
// Auth: "jitsi",
@@ -52,13 +52,13 @@ var (
// Config is the input to [New].
type Config struct {
// --- built-in auth mode ---
// Auth is the name of a registered auth provider ("jitsi", "telemost", "jazz", "wbstream").
// Auth is the name of a registered auth provider ("jitsi", "telemost", "wbstream").
// When set, RoomID is forwarded to the provider as the room reference.
Auth string
RoomID string
// --- direct engine mode (Auth == "") ---
// Engine selects the SFU protocol ("livekit", "goolom", "salutejazz").
// Engine selects the SFU protocol ("livekit", "goolom", "jitsi").
// Defaults to "livekit" when Auth is empty.
Engine string
URL string
@@ -77,9 +77,9 @@ type Config struct {
// Session is the library handle returned by [New].
// Call [Session.Dial] to connect and obtain a [net.Conn].
type Session struct {
inner engine.Session
pr *io.PipeReader
pw *io.PipeWriter
inner engine.Session
pr *io.PipeReader
pw *io.PipeWriter
authProvider auth.Provider
authCfg auth.Config
}
@@ -241,7 +241,7 @@ func (s *Session) SetShouldReconnect(fn func() bool) {
// CreateRoom creates a new room via the auth provider and returns the room ID.
// Only works when the session was created with Auth set to a provider that
// supports room creation (wbstream, jazz). Returns [ErrRoomCreationUnsupported]
// supports room creation (wbstream). Returns [ErrRoomCreationUnsupported]
// for providers that don't support it (e.g. telemost).
func CreateRoom(ctx context.Context, authName string) (string, error) {
p, err := auth.Get(authName)

View File

@@ -29,7 +29,7 @@
// }
//
// Call [RegisterDefaults] once at program start to register the built-in
// carriers (jitsi, telemost, jazz, wbstream) and transports (datachannel,
// carriers (jitsi, telemost, wbstream) and transports (datachannel,
// videochannel, seichannel, vp8channel).
package tunnel
@@ -72,11 +72,11 @@ type TrafficFunc = server.TrafficFunc
type Config struct {
// --- carrier selection ---
Transport string // datachannel, videochannel, seichannel, vp8channel
Carrier string // jitsi, telemost, jazz, wbstream, none
Carrier string // jitsi, telemost, wbstream, none
RoomURL string // conference room identifier for the carrier
// --- direct engine mode (Carrier == "none") ---
Engine string // livekit, goolom, salutejazz, jitsi
Engine string // livekit, goolom, jitsi
URL string
Token string

View File

@@ -85,18 +85,14 @@ validate_key() {
echo "Select auth provider:"
echo " 1) jitsi"
echo " 2) telemost"
echo " 3) jazz"
echo " 4) wbstream"
read -p "Enter choice [1-4, default: 1]: " AUTH_CHOICE
echo " 3) wbstream"
read -p "Enter choice [1-3, default: 1]: " AUTH_CHOICE
case "$AUTH_CHOICE" in
2)
AUTH="telemost"
;;
3)
AUTH="jazz"
;;
4)
AUTH="wbstream"
;;
*)

View File

@@ -55,7 +55,7 @@ case "$mode" in
srv|cnc) ;;
*) die "set OLCRTC_MODE to srv or cnc" ;;
esac
[ -n "$carrier" ] || die "set OLCRTC_CARRIER (e.g. jitsi, telemost, jazz, wbstream)"
[ -n "$carrier" ] || die "set OLCRTC_CARRIER (e.g. jitsi, telemost, wbstream)"
[ -n "$transport" ] || die "set OLCRTC_TRANSPORT (e.g. datachannel, videochannel, seichannel, vp8channel)"
make_key() {
@@ -67,30 +67,7 @@ make_key() {
}
if [ -z "$room_id" ]; then
case "$carrier" in
jazz)
[ "$mode" = "srv" ] || die "set OLCRTC_ROOM_ID to the server room identifier"
echo "olcrtc-entrypoint: OLCRTC_ROOM_ID not set, generating room..." >&2
gen_config="/tmp/olcrtc-gen.yaml"
cat > "$gen_config" <<GENEOF
mode: gen
auth:
provider: "$carrier"
net:
dns: "$dns_server"
gen:
amount: 1
data: "$data_dir"
GENEOF
room_id=$(/usr/local/bin/olcrtc "$gen_config")
[ -n "$room_id" ] || die "room generation failed for carrier '$carrier'"
echo "olcrtc-entrypoint: generated room ID: $room_id" >&2
rm -f "$gen_config"
;;
*)
die "set OLCRTC_ROOM_ID to the room identifier"
;;
esac
die "set OLCRTC_ROOM_ID to the room identifier"
fi
if [ -z "$key" ]; then

View File

@@ -81,18 +81,14 @@ validate_key() {
echo "Select carrier:"
echo " 1) jitsi"
echo " 2) telemost"
echo " 3) jazz"
echo " 4) wbstream"
read -p "Enter choice [1-4, default: 1]: " CARRIER_CHOICE
echo " 3) wbstream"
read -p "Enter choice [1-3, default: 1]: " CARRIER_CHOICE
case "$CARRIER_CHOICE" in
2)
CARRIER="telemost"
;;
3)
CARRIER="jazz"
;;
4)
CARRIER="wbstream"
;;
*)
@@ -130,27 +126,7 @@ echo ""
GEN_ROOM=0
if [ "$CARRIER" = "jazz" ]; then
echo "Room options:"
echo " 1) Auto-generate new room (recommended)"
echo " 2) Use specific room ID"
read -p "Enter choice [1-2, default: 1]: " ROOM_CHOICE
case "$ROOM_CHOICE" in
2)
read -p "Enter Room ID: " ROOM_ID
if [ -z "$ROOM_ID" ]; then
echo "[X] Room ID cannot be empty"
exit 1
fi
;;
*)
GEN_ROOM=1
ROOM_ID=""
echo "[*] Will generate room before starting server"
;;
esac
elif [ "$CARRIER" = "jitsi" ]; then
if [ "$CARRIER" = "jitsi" ]; then
read -p "Jitsi base URL [default: https://meet.small-dm.ru/]: " JITSI_BASE_INPUT
JITSI_BASE_URL=${JITSI_BASE_INPUT:-https://meet.small-dm.ru/}
JITSI_BASE_URL="${JITSI_BASE_URL%/}"