Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5bd8064158 | |||
| 22a95af04d | |||
| bf7e17921d | |||
| 3393858552 | |||
| e7c12b4253 | |||
| 45a69aaaf0 |
@@ -1,4 +1,4 @@
|
||||
FROM livekit/livekit-server:v1.8.0
|
||||
FROM livekit/livekit-server:v1.9.0
|
||||
|
||||
# We inject the nip.io certificate manually because the livekit chart doesn't support volume mounting
|
||||
COPY rootCA.pem /etc/ssl/certs/
|
||||
|
||||
@@ -142,4 +142,4 @@ Once the Kubernetes cluster is ready, start the application stack locally:
|
||||
$ make start-tilt-keycloak
|
||||
```
|
||||
|
||||
Monitor Tilt’s progress at [http://localhost:10350/](http://localhost:10350/). After Tilt actions finish, you can access the app at [https://visio.127.0.0.1.nip.io/](https://visio.127.0.0.1.nip.io/).
|
||||
Monitor Tilt’s progress at [http://localhost:10350/](http://localhost:10350/). After Tilt actions finish, you can access the app at [https://meet.127.0.0.1.nip.io/](https://meet.127.0.0.1.nip.io/).
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
"""Visible room join agent (for connection/testing) + JSON speaker intervals per participant."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, List, Optional
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
from io import BytesIO
|
||||
from minio import Minio
|
||||
from minio.error import S3Error
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from livekit import api, rtc
|
||||
from livekit.agents import (
|
||||
AutoSubscribe,
|
||||
JobContext,
|
||||
JobRequest,
|
||||
WorkerOptions,
|
||||
WorkerPermissions,
|
||||
cli,
|
||||
)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
logger = logging.getLogger("visible-joiner")
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.propagate = False
|
||||
|
||||
if not logger.handlers:
|
||||
_h = logging.StreamHandler()
|
||||
_h.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s %(name)s - %(message)s"))
|
||||
logger.addHandler(_h)
|
||||
VISIBLE_AGENT_NAME = os.getenv("VISIBLE_AGENT_NAME", "visible-joiner")
|
||||
|
||||
|
||||
class SpeakerTracker:
|
||||
def __init__(self, room_name: str, write_json: bool = True):
|
||||
self.room_name = room_name
|
||||
self.active_since: Dict[str, datetime] = {}
|
||||
self.by_participant: Dict[str, List[dict]] = {}
|
||||
self.write_json_flag = write_json
|
||||
|
||||
ts = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
|
||||
outdir = pathlib.Path("./speaker_logs")
|
||||
outdir.mkdir(parents=True, exist_ok=True)
|
||||
self.json_path: Optional[pathlib.Path] = outdir / f"speakers_{room_name}_{ts}.json" if write_json else None
|
||||
|
||||
def _now(self) -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
def _emit_interval(self, identity: str, start: datetime, end: datetime):
|
||||
dur = max(0.0, (end - start).total_seconds())
|
||||
seg = {
|
||||
"start_iso": start.isoformat(),
|
||||
"end_iso": end.isoformat(),
|
||||
"duration_sec": round(dur, 3),
|
||||
}
|
||||
self.by_participant.setdefault(identity, []).append(seg)
|
||||
|
||||
def update_active_speakers(self, current_identities: List[str]):
|
||||
now = self._now()
|
||||
current = set(current_identities)
|
||||
before = set(self.active_since.keys())
|
||||
|
||||
for ident in current - before:
|
||||
self.active_since[ident] = now
|
||||
|
||||
for ident in before - current:
|
||||
start = self.active_since.pop(ident, None)
|
||||
if start:
|
||||
self._emit_interval(ident, start, now)
|
||||
|
||||
def on_participant_disconnected(self, identity: str):
|
||||
now = self._now()
|
||||
start = self.active_since.pop(identity, None)
|
||||
if start:
|
||||
self._emit_interval(identity, start, now)
|
||||
|
||||
def flush_all(self):
|
||||
now = self._now()
|
||||
for ident, start in list(self.active_since.items()):
|
||||
self._emit_interval(ident, start, now)
|
||||
self.active_since.clear()
|
||||
|
||||
def build_json(self) -> dict:
|
||||
return {
|
||||
"room": self.room_name,
|
||||
"generated_at": self._now().isoformat(),
|
||||
"by_participant": self.by_participant,
|
||||
}
|
||||
|
||||
|
||||
|
||||
def write_json(self):
|
||||
def _as_bool(v: str, default=False):
|
||||
if v is None:
|
||||
return default
|
||||
return v.strip().lower() in ("1", "true", "yes", "y")
|
||||
|
||||
if not self.write_json_flag:
|
||||
return
|
||||
payload = self.build_json()
|
||||
|
||||
minio_client = Minio(
|
||||
endpoint=os.getenv("AWS_S3_ENDPOINT_URL"),
|
||||
access_key=os.getenv("AWS_S3_ACCESS_KEY_ID"),
|
||||
secret_key=os.getenv("AWS_S3_SECRET_ACCESS_KEY"),
|
||||
secure= _as_bool(os.getenv("AWS_S3_SECURE_ACCESS", "false"))
|
||||
)
|
||||
|
||||
bucket = "meet-media-storage"
|
||||
ts = self._now().strftime("%Y%m%dT%H%M%SZ")
|
||||
object_name = f"speaker_logs/{self.room_name}/speakers_{self.room_name}_{ts}.json"
|
||||
|
||||
data = json.dumps(payload, indent=2).encode("utf-8")
|
||||
stream = BytesIO(data)
|
||||
|
||||
try:
|
||||
minio_client.put_object(
|
||||
bucket,
|
||||
object_name,
|
||||
stream,
|
||||
length=len(data),
|
||||
content_type="application/json",
|
||||
)
|
||||
logger.info("Uploaded speaker intervals JSON to s3://%s/%s", bucket, object_name)
|
||||
except S3Error:
|
||||
logger.exception("Failed to upload JSON to bucket=%s object=%s", bucket, object_name)
|
||||
|
||||
|
||||
|
||||
async def entrypoint(ctx: JobContext):
|
||||
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
|
||||
|
||||
lp = ctx.room.local_participant
|
||||
logger.info(
|
||||
"Connected to SFU; room=%s identity=%s sid=%s",
|
||||
ctx.room.name,
|
||||
getattr(lp, "identity", "<unknown>"),
|
||||
getattr(lp, "sid", "<unknown>"),
|
||||
)
|
||||
|
||||
tracker = SpeakerTracker(ctx.room.name, write_json=True)
|
||||
|
||||
def _on_participant_connected(p: rtc.RemoteParticipant):
|
||||
logger.info("remote participant connected: %s (%s)", p.identity, p.sid)
|
||||
|
||||
def _on_active_speakers_changed(speakers: list[rtc.Participant]):
|
||||
idents = [p.identity for p in speakers]
|
||||
logger.info("active speakers changed: %s", idents)
|
||||
tracker.update_active_speakers(idents)
|
||||
|
||||
def _on_participant_disconnected(p: rtc.RemoteParticipant):
|
||||
logger.info("remote participant disconnected: %s", p.identity)
|
||||
tracker.on_participant_disconnected(p.identity)
|
||||
|
||||
if not ctx.proc.userdata.get("events_registered"):
|
||||
ctx.room.on("participant_connected", _on_participant_connected)
|
||||
ctx.room.on("active_speakers_changed", _on_active_speakers_changed)
|
||||
ctx.room.on("participant_disconnected", _on_participant_disconnected)
|
||||
ctx.proc.userdata["events_registered"] = True
|
||||
|
||||
async def cleanup():
|
||||
if ctx.proc.userdata.get("events_registered"):
|
||||
ctx.room.off("participant_connected", _on_participant_connected)
|
||||
ctx.room.off("active_speakers_changed", _on_active_speakers_changed)
|
||||
ctx.room.off("participant_disconnected", _on_participant_disconnected)
|
||||
ctx.proc.userdata["events_registered"] = False
|
||||
tracker.flush_all()
|
||||
tracker.write_json()
|
||||
|
||||
ctx.add_shutdown_callback(cleanup)
|
||||
|
||||
|
||||
|
||||
async def handle_job_request(job_req: JobRequest) -> None:
|
||||
room_name = job_req.room.name
|
||||
agent_identity = f"{VISIBLE_AGENT_NAME}-{room_name}"
|
||||
|
||||
async with api.LiveKitAPI() as lk:
|
||||
try:
|
||||
resp = await lk.room.list_participants(
|
||||
list=api.ListParticipantsRequest(room=room_name)
|
||||
)
|
||||
already_present = any(
|
||||
p.kind == rtc.ParticipantKind.PARTICIPANT_KIND_AGENT
|
||||
and p.identity == agent_identity
|
||||
for p in resp.participants
|
||||
)
|
||||
if already_present:
|
||||
logger.info("Agent already in the room '%s' — reject", room_name)
|
||||
await job_req.reject()
|
||||
else:
|
||||
logger.info("Accept job for '%s' — identity=%s", room_name, agent_identity)
|
||||
await job_req.accept(identity=agent_identity)
|
||||
except Exception:
|
||||
logger.exception("Error treating the job for '%s'", room_name)
|
||||
await job_req.reject()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli.run_app(
|
||||
WorkerOptions(
|
||||
entrypoint_fnc=entrypoint,
|
||||
request_fnc=handle_job_request,
|
||||
agent_name=VISIBLE_AGENT_NAME,
|
||||
permissions=WorkerPermissions(),
|
||||
)
|
||||
)
|
||||
@@ -29,6 +29,10 @@ body:has(.lk-video-conference) #crisp-chatbox-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body:has(.lk-video-conference) #crisp-chatbox > * > div[role='button'] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@keyframes slide-full {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
environments:
|
||||
dev-keycloak:
|
||||
values:
|
||||
- version: 0.0.1
|
||||
- env.d/{{ .Environment.Name }}/values.secrets.yaml
|
||||
dev-dinum:
|
||||
values:
|
||||
- version: 0.0.1
|
||||
- env.d/{{ .Environment.Name }}/values.secrets.yaml
|
||||
dev:
|
||||
values:
|
||||
- version: 0.0.1
|
||||
- env.d/{{ .Environment.Name }}/values.secrets.yaml
|
||||
|
||||
repositories:
|
||||
- name: livekit
|
||||
url: https://helm.livekit.io
|
||||
|
||||
releases:
|
||||
- name: extra
|
||||
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
|
||||
missingFileHandler: Warn
|
||||
namespace: {{ .Namespace }}
|
||||
chart: ./extra
|
||||
secrets:
|
||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||
values:
|
||||
- env.d/{{ .Environment.Name }}/values.meet.yaml.gotmpl
|
||||
- addRedirect: {{ .Values | get "addRedirect" "False" }}
|
||||
enablePermanentRedirect: {{ .Values | get "enablePermanentRedirect" "False"}}
|
||||
oldDomain: {{ .Values | get "oldDomain" "demo.com" }}
|
||||
newDomain: {{ .Values | get "newDomain" "demo.com" }}
|
||||
- realm: |
|
||||
{{ readFile "../../docker/auth/realm.json" | replace "http://localhost:3200" "https://meet.127.0.0.1.nip.io" | indent 8 }}
|
||||
|
||||
- name: meet
|
||||
version: {{ .Values.version }}
|
||||
namespace: {{ .Namespace }}
|
||||
missingFileHandler: Warn
|
||||
chart: ./meet
|
||||
values:
|
||||
- env.d/{{ .Environment.Name }}/values.meet.yaml.gotmpl
|
||||
- env.d/{{ .Environment.Name }}/values.secrets.yaml
|
||||
secrets:
|
||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||
|
||||
- name: livekit
|
||||
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
|
||||
missingFileHandler: Warn
|
||||
namespace: {{ .Namespace }}
|
||||
chart: livekit/livekit-server
|
||||
values:
|
||||
- env.d/{{ .Environment.Name }}/values.livekit.yaml.gotmpl
|
||||
- env.d/{{ .Environment.Name }}/values.secrets.yaml
|
||||
secrets:
|
||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||
|
||||
- name: livekit-egress
|
||||
installed: {{ regexMatch "^dev.*" .Environment.Name | toYaml }}
|
||||
missingFileHandler: Warn
|
||||
namespace: {{ .Namespace }}
|
||||
chart: livekit/egress
|
||||
values:
|
||||
- env.d/{{ .Environment.Name }}/values.egress.yaml.gotmpl
|
||||
- env.d/{{ .Environment.Name }}/values.secrets.yaml
|
||||
secrets:
|
||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||
@@ -0,0 +1,69 @@
|
||||
environments:
|
||||
dev-keycloak:
|
||||
values:
|
||||
- version: 0.0.1
|
||||
- env.d/dev-keycloak/values.secrets.yaml
|
||||
dev-dinum:
|
||||
values:
|
||||
- version: 0.0.1
|
||||
- env.d/dev-dinum/values.secrets.yaml
|
||||
dev:
|
||||
values:
|
||||
- version: 0.0.1
|
||||
- env.d/dev/values.secrets.yaml
|
||||
|
||||
---
|
||||
repositories:
|
||||
- name: livekit
|
||||
url: https://helm.livekit.io
|
||||
|
||||
releases:
|
||||
- name: extra
|
||||
installed: {{ regexMatch "^dev.*" .Environment.Name }}
|
||||
missingFileHandler: Warn
|
||||
namespace: {{ .Namespace }}
|
||||
chart: ./extra
|
||||
secrets:
|
||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||
values:
|
||||
- env.d/{{ .Environment.Name }}/values.meet.yaml.gotmpl
|
||||
- addRedirect: {{ .Values | get "addRedirect" "False" }}
|
||||
- enablePermanentRedirect: {{ .Values | get "enablePermanentRedirect" "False"}}
|
||||
- oldDomain: {{ .Values | get "oldDomain" "demo.com" }}
|
||||
- newDomain: {{ .Values | get "newDomain" "demo.com" }}
|
||||
- realm: |
|
||||
{{ readFile "../../docker/auth/realm.json" | replace "http://localhost:3200" "https://meet.127.0.0.1.nip.io" | indent 8 }}
|
||||
|
||||
|
||||
- name: meet
|
||||
version: {{ .Values.version }}
|
||||
namespace: {{ .Namespace }}
|
||||
missingFileHandler: Warn
|
||||
chart: ./meet
|
||||
values:
|
||||
- env.d/{{ .Environment.Name }}/values.meet.yaml.gotmpl
|
||||
- env.d/{{ .Environment.Name }}/values.secrets.yaml
|
||||
secrets:
|
||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||
|
||||
- name: livekit
|
||||
installed: {{ regexMatch "^dev.*" .Environment.Name }}
|
||||
missingFileHandler: Warn
|
||||
namespace: {{ .Namespace }}
|
||||
chart: livekit/livekit-server
|
||||
values:
|
||||
- env.d/{{ .Environment.Name }}/values.livekit.yaml.gotmpl
|
||||
- env.d/{{ .Environment.Name }}/values.secrets.yaml
|
||||
secrets:
|
||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||
|
||||
- name: livekit-egress
|
||||
installed: {{ regexMatch "^dev.*" .Environment.Name }}
|
||||
missingFileHandler: Warn
|
||||
namespace: {{ .Namespace }}
|
||||
chart: livekit/egress
|
||||
values:
|
||||
- env.d/{{ .Environment.Name }}/values.egress.yaml.gotmpl
|
||||
- env.d/{{ .Environment.Name }}/values.secrets.yaml
|
||||
secrets:
|
||||
- env.d/{{ .Environment.Name }}/secrets.enc.yaml
|
||||
Reference in New Issue
Block a user