mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +00:00
ssh video-<code>@ joins a PairUX/LiveKit call rendered as truecolor ASCII (▀ half-blocks, 2 pixels per cell); video@ prompts for a code. Codes are minted by PairUX only. Pipeline: VP8 RTP → PLI keyframe requests → ivfwriter remux → ffmpeg decode/scale → RGB24 → ANSI → bubbletea over the SSH PTY. Subscriber-only, no audio in v1. ssh agent@ opens a persisted chat with the operator's agent — AGENTBBS_AGENT_CMD runs per message (stdin→stdout), e.g. `claude -p`. ssh <member>@ with someone else's name prints a classic finger card (.plan, member since, last seen) and disconnects; your own name still enters the hub. cmd/lkpublish: dev publisher for testing (explicit -fps pacing; lksdk IVF replay mispaces from file timebase alone, measured 1fps from a 15fps file; dimensions required or dynacast pauses the track). Verified end-to-end against livekit-server --dev: 128k truecolor cells / 20k distinct colors streamed over a real SSH session; chat round-trip and finger card verified over SSH. Go toolchain pinned to 1.26 via mise.toml (lksdk requirement). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
1.9 KiB
Go
68 lines
1.9 KiB
Go
// Command lkpublish is a dev tool: it publishes a VP8 IVF file into a
|
|
// LiveKit room so the video-<code>@ SSH route has something to render.
|
|
//
|
|
// ffmpeg -f lavfi -i testsrc=size=320x240:rate=15 -t 60 -c:v libvpx test.ivf
|
|
// go run ./cmd/lkpublish -room test123 -file test.ivf
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"os"
|
|
"os/signal"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/log"
|
|
"github.com/livekit/protocol/livekit"
|
|
lksdk "github.com/livekit/server-sdk-go/v2"
|
|
)
|
|
|
|
func env(k, def string) string {
|
|
if v := os.Getenv(k); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
func main() {
|
|
room := flag.String("room", "test123", "room/call code to publish into")
|
|
file := flag.String("file", "test.ivf", "VP8 IVF file to stream")
|
|
fps := flag.Int("fps", 15, "frame rate to pace the file at")
|
|
flag.Parse()
|
|
|
|
url := env("LIVEKIT_URL", "ws://localhost:7880")
|
|
key := env("LIVEKIT_API_KEY", "devkey")
|
|
secret := env("LIVEKIT_API_SECRET", "secret")
|
|
|
|
r, err := lksdk.ConnectToRoom(url, lksdk.ConnectInfo{
|
|
APIKey: key,
|
|
APISecret: secret,
|
|
RoomName: *room,
|
|
ParticipantIdentity: "lkpublish",
|
|
}, &lksdk.RoomCallback{})
|
|
if err != nil {
|
|
log.Fatal("connect", "err", err)
|
|
}
|
|
defer r.Disconnect()
|
|
|
|
// Explicit frame duration: don't trust IVF timebase interpretation.
|
|
track, err := lksdk.NewLocalFileTrack(*file,
|
|
lksdk.ReaderTrackWithFrameDuration(time.Second/time.Duration(*fps)))
|
|
if err != nil {
|
|
log.Fatal("track", "err", err)
|
|
}
|
|
// Width/height/source matter: without dimensions the SFU's dynacast has
|
|
// no layer to subscribe to and pauses the track entirely.
|
|
if _, err := r.LocalParticipant.PublishTrack(track, &lksdk.TrackPublicationOptions{
|
|
Name: "test-pattern",
|
|
Source: livekit.TrackSource_CAMERA,
|
|
VideoWidth: 320,
|
|
VideoHeight: 240,
|
|
}); err != nil {
|
|
log.Fatal("publish", "err", err)
|
|
}
|
|
log.Info("publishing", "room", *room, "file", *file)
|
|
|
|
sig := make(chan os.Signal, 1)
|
|
signal.Notify(sig, os.Interrupt)
|
|
<-sig
|
|
}
|