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>
52 lines
1.4 KiB
Go
52 lines
1.4 KiB
Go
// Package ascii converts raw video frames into truecolor terminal art.
|
|
//
|
|
// Each character cell renders two vertical pixels using the upper-half block
|
|
// (▀): foreground colors the top pixel, background the bottom — the same
|
|
// technique doom-ascii uses. A WxH terminal therefore displays a Wx(2H)
|
|
// pixel image.
|
|
package ascii
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// FrameRGB renders a packed RGB24 frame (w*h*3 bytes, as ffmpeg's rawvideo
|
|
// rgb24 emits) sized exactly for the target cell grid: w columns, h*2 rows
|
|
// of pixels. Rows are joined with \r\n so the output is PTY-safe.
|
|
func FrameRGB(buf []byte, w, h int) string {
|
|
if len(buf) < w*h*3 || w <= 0 || h <= 0 {
|
|
return ""
|
|
}
|
|
rows := h / 2
|
|
var b strings.Builder
|
|
b.Grow(rows * w * 40)
|
|
for row := 0; row < rows; row++ {
|
|
top := row * 2
|
|
bot := top + 1
|
|
for x := 0; x < w; x++ {
|
|
ti := (top*w + x) * 3
|
|
bi := (bot*w + x) * 3
|
|
fmt.Fprintf(&b, "\x1b[38;2;%d;%d;%dm\x1b[48;2;%d;%d;%dm▀",
|
|
buf[ti], buf[ti+1], buf[ti+2],
|
|
buf[bi], buf[bi+1], buf[bi+2])
|
|
}
|
|
b.WriteString("\x1b[0m")
|
|
if row != rows-1 {
|
|
b.WriteString("\r\n")
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// FitEven clamps a terminal geometry to an even pixel height for the
|
|
// half-block renderer and returns pixel dimensions (pw, ph) for the decoder.
|
|
func FitEven(cols, rows int) (pw, ph int) {
|
|
if cols < 8 {
|
|
cols = 8
|
|
}
|
|
if rows < 4 {
|
|
rows = 4
|
|
}
|
|
return cols, (rows - 1) * 2 // leave one status line
|
|
}
|