package main
import (
"bytes"
"encoding/binary"
"fmt"
"image"
"image/color"
"image/png"
"net"
"net/http"
"os"
"g.fc.run/theater/cabletest/internal/drm"
)
const streamAddr = ":8080"
const page = `
cabletest
`
func serveStream() error {
ln, err := net.Listen("tcp", streamAddr)
if err != nil {
return err
}
host, err := os.Hostname()
if err != nil {
host = "localhost"
}
fmt.Printf("panel stream at http://%s%s/\n", host, streamAddr)
mux := http.NewServeMux()
mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, page)
})
mux.HandleFunc("/frames", frames)
go http.Serve(ln, mux)
return nil
}
func frames(w http.ResponseWriter, r *http.Request) {
g, err := openGrabber()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer g.close()
cur, pitch, pw, ph, err := g.rawFrame(nil)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
if err := send(w, image.Pt(0, 0), decode(cur, pitch, pw, ph)); err != nil {
return
}
ctx := r.Context()
prev := make([]byte, len(cur))
for {
prev, cur = cur, prev
for {
if ctx.Err() != nil {
return
}
if cur, pitch, pw, ph, err = g.rawFrame(cur); err != nil {
fmt.Fprintf(os.Stderr, "stream %s: %v\n", r.RemoteAddr, err)
return
}
rects := diffRects(prev, cur, pitch, pw, ph)
if len(rects) == 0 {
continue
}
for _, rect := range rects {
if err := send(w, rect.Min, maskRect(prev, cur, pitch, ph, rect)); err != nil {
return
}
}
break
}
}
}
// Encoding happens inside the vblank budget, and on rect-sized parts the
// dense level trades 3-6x the cpu for a few percent of size — enough to spill
// past a vblank on the once-per-second bursts and stutter the stream.
var encoder = png.Encoder{CompressionLevel: png.DefaultCompression}
func send(w http.ResponseWriter, at image.Point, img *image.NRGBA) error {
var buf bytes.Buffer
if err := encoder.Encode(&buf, img); err != nil {
return err
}
var hdr [12]byte
binary.LittleEndian.PutUint32(hdr[0:], uint32(at.X))
binary.LittleEndian.PutUint32(hdr[4:], uint32(at.Y))
binary.LittleEndian.PutUint32(hdr[8:], uint32(buf.Len()))
if _, err := w.Write(hdr[:]); err != nil {
return err
}
if _, err := w.Write(buf.Bytes()); err != nil {
return err
}
w.(http.Flusher).Flush()
return nil
}
// Distant regions changing together (a spinner up top, counters at the foot)
// would union into one near-full-height box, so changed portrait rows are
// merged into bands only across small gaps and each band gets its own box.
const bandGap = 32
// Compares in panel space, where a portrait column is one contiguous row, and
// reports the changed bounding boxes in portrait coordinates.
func diffRects(prev, cur []byte, pitch, pw, ph int) []image.Rectangle {
var rows []int
for row := 0; row < ph; row++ {
if !bytes.Equal(prev[row*pitch:row*pitch+pw*4], cur[row*pitch:row*pitch+pw*4]) {
rows = append(rows, row)
}
}
if len(rows) == 0 {
return nil
}
changed := make([]bool, pw)
for _, row := range rows {
a := prev[row*pitch : row*pitch+pw*4]
b := cur[row*pitch : row*pitch+pw*4]
for y := 0; y < pw; y++ {
if !changed[y] && !bytes.Equal(a[y*4:y*4+4], b[y*4:y*4+4]) {
changed[y] = true
}
}
}
var rects []image.Rectangle
for y := 0; y < pw; {
if !changed[y] {
y++
continue
}
y0, y1 := y, y+1
for next := y1; next < pw && next < y1+bandGap; next++ {
if changed[next] {
y1 = next + 1
}
}
rMin, rMax := -1, -1
for _, row := range rows {
if !bytes.Equal(prev[row*pitch+y0*4:row*pitch+y1*4], cur[row*pitch+y0*4:row*pitch+y1*4]) {
if rMin < 0 {
rMin = row
}
rMax = row
}
}
rects = append(rects, image.Rect(ph-1-rMax, y0, ph-rMin, y1))
y = y1
}
return rects
}
// The box holds only the pixels that changed, transparent everywhere else, so
// the client composites it straight over the last frame.
func maskRect(prev, cur []byte, pitch, ph int, rect image.Rectangle) *image.NRGBA {
img := image.NewNRGBA(image.Rect(0, 0, rect.Dx(), rect.Dy()))
for x := rect.Min.X; x < rect.Max.X; x++ {
row := (ph - 1 - x) * pitch
for y := rect.Min.Y; y < rect.Max.Y; y++ {
o := row + y*4
if bytes.Equal(prev[o:o+4], cur[o:o+4]) {
continue
}
v := uint32(cur[o]) | uint32(cur[o+1])<<8 |
uint32(cur[o+2])<<16 | uint32(cur[o+3])<<24
cr, cg, cb := drm.Unpack(v)
img.SetNRGBA(x-rect.Min.X, y-rect.Min.Y, color.NRGBA{R: cr, G: cg, B: cb, A: 255})
}
}
return img
}