Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,11 @@ restores the image but never the schema, so keep migrations additive.
publishes each upload as-is -- a camera's HEIF keyframe (and its substream's,
sent as the optional `thumb` part), checked by `internal/keyframe`, or a
legacy JPEG with its metadata dropped. **Nothing a camera sends is
re-encoded.**
re-encoded.** Each frame's brightness is measured from the decode it is
checked with, and the home page's mosaic (`Store.Showcase`) leaves out
flat, black or blown-out frames and any camera first seen less than 30
days ago (`cameras`, migration 022), so a new camera cannot deface the
front page.
- `internal/wall`, `internal/wallsocket` — the wall's JSON and the frame socket
(a small JSON protocol at `/api/v1/wall/socket`), with signed
grants keyed by `WALL_GRANT_KEY`.
Expand Down
1 change: 1 addition & 0 deletions service/conformance/conformance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ func start(t *testing.T, surface string) *suite {
}
for _, mac := range s.macs {
s.db.Exec(context.Background(), "DELETE FROM snapshots WHERE mac_address = $1", mac)
s.db.Exec(context.Background(), "DELETE FROM cameras WHERE mac_key = lower(translate($1, ':-', ''))", mac)
}
s.db.Close(context.Background())
})
Expand Down
72 changes: 68 additions & 4 deletions service/conformance/wall_api_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package conformance

import (
"bytes"
"context"
"encoding/json"
"fmt"
"image"
"image/color"
imagejpeg "image/jpeg"
mathrand "math/rand/v2"
"reflect"
"regexp"
"strings"
Expand Down Expand Up @@ -33,10 +39,16 @@ func (s *suite) wallSetup() {

// frames uploads count frames for one camera, oldest first, and returns their ids.
func (s *suite) frames(mac string, count int, fields map[string]string) []string {
return s.framesOf(mac, count, fields, nil)
}

// framesOf is frames with a picture of the caller's: nil sends the default
// JPEG, which is a header and padding and which the wall refuses to publish.
func (s *suite) framesOf(mac string, count int, fields map[string]string, f *file) []string {
s.t.Helper()
var ids []string
for range count {
r := s.upload(upload{mac: &mac, headers: map[string]string{"X-Forwarded-For": whitelisted}, fields: fields})
r := s.upload(upload{mac: &mac, file: f, headers: map[string]string{"X-Forwarded-For": whitelisted}, fields: fields})
if r.StatusCode != 201 {
s.t.Fatalf("upload: %d %q", r.StatusCode, r.Header.Get("X-Error"))
}
Expand Down Expand Up @@ -80,16 +92,68 @@ func assertKeys(t *testing.T, got, want []string, what string) {
}
}

func TestTheMosaicIsTheNewestFrameOfEachCameraWithAThumbGrant(t *testing.T) {
// picture is a JPEG with something in it -- a gradient under noise -- over
// the upload's minimum size, so the wall publishes it and measures it as a
// scene rather than a blank frame.
func picture() *file {
img := image.NewRGBA(image.Rect(0, 0, 320, 180))
rng := mathrand.New(mathrand.NewPCG(1, 2))
for y := range 180 {
for x := range 320 {
v := uint8(x*200/320 + rng.IntN(48))
img.SetRGBA(x, y, color.RGBA{v, uint8(y), 255 - v, 255})
}
}
var buf bytes.Buffer
imagejpeg.Encode(&buf, img, &imagejpeg.Options{Quality: 95})
return &file{name: "snapshot.jpg", declared: str("image/jpeg"), data: buf.Bytes()}
}

// measured waits for the wall to publish and measure a frame.
func (s *suite) measured(id string) {
s.t.Helper()
for range 100 {
var done bool
if err := s.db.QueryRow(context.Background(),
"SELECT luma_p50 IS NOT NULL FROM snapshots WHERE public_id = $1", id).Scan(&done); err != nil {
s.t.Fatal(err)
}
if done {
return
}
time.Sleep(100 * time.Millisecond)
}
s.t.Fatalf("%s was not published and measured in 10 s", id)
}

// The mosaic is the home page's, not the wall's: a camera reaches it once it
// has been uploading for a month on many days (snapshots.Showcase), so a
// camera that has just uploaded is not on it until the database says it is
// established.
func TestTheMosaicIsTheNewestFrameOfEachEstablishedCameraWithAThumbGrant(t *testing.T) {
s := start(t, "wall")
s.wallSetup()
ids := s.frames(s.freshMAC(), 2, map[string]string{"soc": "hi3516ev300", "sensor": "imx335"})
mac := s.freshMAC()
ids := s.framesOf(mac, 2, map[string]string{"soc": "hi3516ev300", "sensor": "imx335"}, picture())
s.measured(ids[1])

var body struct {
Variant string
Grant *string
Tiles []map[string]any
}
s.wallJSON("/api/v1/wall/mosaic.json", &body)
for _, tile := range body.Tiles {
if tile["id"] == ids[0] || tile["id"] == ids[1] {
t.Error("a camera that started uploading a moment ago is on the front page")
}
}
if _, err := s.db.Exec(context.Background(), `UPDATE cameras
SET first_seen = now() - interval '31 days', days = 31 WHERE mac_key = lower(translate($1, ':-', ''))`, mac); err != nil {
t.Fatal(err)
}

body.Tiles = nil
r := s.wallJSON("/api/v1/wall/mosaic.json", &body)
s.assertWallHeaders(r, "mosaic")
assertKeys(t, keys(t, r.body), []string{"variant", "grant", "tiles"}, "mosaic")
Expand All @@ -107,7 +171,7 @@ func TestTheMosaicIsTheNewestFrameOfEachCameraWithAThumbGrant(t *testing.T) {
}
want := map[string]any{"id": ids[1], "soc": "hi3516ev300", "sensor": "imx335"}
if !reflect.DeepEqual(ours, want) {
t.Errorf("the newest frame of a camera that just uploaded: %v, want %v", ours, want)
t.Errorf("the newest frame of an established camera: %v, want %v", ours, want)
}
assertKeys(t, keys(t, firstObject(t, r.body, "tiles")), []string{"id", "soc", "sensor"}, "tile")
}
Expand Down
39 changes: 39 additions & 0 deletions service/internal/db/migrations/022_wall_showcase.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
-- What the home page's mosaic may show (internal/snapshots, Showcase).
--
-- The mosaic is the first thing a visitor to openipc.org sees, and the wall
-- publishes whatever a camera sends. Two kinds of frame do not belong there:
--
-- A frame with nothing in it. A lens cap, a sensor stuck white, an IR scene
-- with no light: 7.5% of the frames on the wall on 2026-10-03 were flat grey,
-- white, black or blown out. Each frame's brightness percentiles over a 64x36
-- greyscale copy are measured once, from the decode the frame is checked
-- with, and the mosaic leaves out a camera whose newest frame is flat. The
-- rule is applied when the mosaic is read, so moving a threshold needs no
-- recomputation.
--
-- A camera nobody knows yet. Anyone can make a camera upload anything, and a
-- new one could put a picture of its choosing on the front page within
-- fifteen minutes. A camera appears there only once it has been uploading
-- for a month, on many separate days -- one upload and a month of silence is
-- not a camera anyone has watched. That needs a camera's history, and
-- snapshots are purged after two days, so cameras keeps it: the first frame
-- the wall accepted, and on how many UTC days it has accepted one. Written
-- by snapshots.Store.MarkGenerated, so an upload the wall refused counts for
-- nothing; never purged, one row per camera.
ALTER TABLE snapshots ADD COLUMN luma_p5 smallint;
ALTER TABLE snapshots ADD COLUMN luma_p50 smallint;
ALTER TABLE snapshots ADD COLUMN luma_p95 smallint;

CREATE TABLE cameras (
mac_key text PRIMARY KEY,
first_seen timestamptz NOT NULL,
last_day date NOT NULL,
days integer NOT NULL
);

-- The frames on the wall at the moment this runs: those with a picture (a
-- refused upload has no dimensions).
INSERT INTO cameras (mac_key, first_seen, last_day, days)
SELECT mac_key, min(created_at), max((created_at AT TIME ZONE 'UTC')::date),
count(DISTINCT (created_at AT TIME ZONE 'UTC')::date)
FROM snapshots WHERE width IS NOT NULL GROUP BY mac_key;
27 changes: 14 additions & 13 deletions service/internal/keyframe/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,18 @@ var ErrCheckTimeout = errors.New("the decode check timed out")

// Check has ffmpeg decode the frame exactly as a browser will receive it --
// the parameter sets from the configuration record followed by the access
// unit -- and requires one picture out with no decode error.
// unit -- and requires one picture out with no decode error. That picture is
// scaled to a LumaW x LumaH full-range greyscale copy on the way out, which is
// what the frame's brightness (Luma) is measured on.
//
// Parse proves the file is shaped like a keyframe; only a decoder proves the
// bitstream inside is one. The wall passes these bytes on untouched to
// visitors' hardware decoders, so one that will not decode here is not
// published. Nothing Check produces is kept.
func Check(ctx context.Context, ffmpeg string, f *Frame) error {
// published. Nothing Check produces is kept but the measurement.
func Check(ctx context.Context, ffmpeg string, f *Frame) (Luma, error) {
stream, err := AnnexB(f)
if err != nil {
return err
return Luma{}, err
}
format := "h264"
if f.HEVC {
Expand All @@ -39,23 +41,22 @@ func Check(ctx context.Context, ffmpeg string, f *Frame) error {
ctx, cancel := context.WithTimeout(ctx, CheckTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, ffmpeg, "-nostdin", "-hide_banner", "-v", "error", "-xerror", "-err_detect", "explode",
"-f", format, "-i", "pipe:0", "-frames:v", "1", "-f", "framemd5", "pipe:1")
"-f", format, "-i", "pipe:0", "-frames:v", "1",
"-vf", fmt.Sprintf("scale=%d:%d:out_range=full,format=gray", LumaW, LumaH), "-f", "rawvideo", "pipe:1")
cmd.Stdin = bytes.NewReader(stream)
var stdout, stderr bytes.Buffer
cmd.Stdout, cmd.Stderr = &stdout, &stderr
if err := cmd.Run(); err != nil {
if parent.Err() == nil && errors.Is(ctx.Err(), context.DeadlineExceeded) {
return ErrCheckTimeout
return Luma{}, ErrCheckTimeout
}
return fmt.Errorf("%s: %w: %s", ffmpeg, err, strings.TrimSpace(stderr.String()))
return Luma{}, fmt.Errorf("%s: %w: %s", ffmpeg, err, strings.TrimSpace(stderr.String()))
}
if msg := strings.TrimSpace(stderr.String()); msg != "" {
return fmt.Errorf("decoder complained: %s", msg)
return Luma{}, fmt.Errorf("decoder complained: %s", msg)
}
for _, line := range strings.Split(stdout.String(), "\n") {
if line != "" && !strings.HasPrefix(line, "#") {
return nil
}
if stdout.Len() != LumaW*LumaH {
return Luma{}, errors.New("no picture decoded")
}
return errors.New("no picture decoded")
return lumaOf(stdout.Bytes()), nil
}
21 changes: 11 additions & 10 deletions service/internal/keyframe/jpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
"image/jpeg"
)

// StripJPEG returns a JPEG without its application and comment segments, and
// the picture's size. No pixel changes: every entropy-coded scan is copied
// StripJPEG returns a JPEG without its application and comment segments, the
// picture's size, and its brightness (Luma). No pixel changes: every entropy-coded scan is copied
// verbatim.
//
// Only cameras that cannot be updated still send JPEG, and the wall keeps
Expand All @@ -18,22 +18,23 @@ import (
// wherever they sit, between scans of a progressive file as well as before
// the first, and the file must run to its end-of-image marker.
//
// The result is then decoded once and thrown away. The browser is handed
// these bytes as they are, so a file cut short or corrupted inside a scan
// would otherwise be published and paint nothing.
// The result is then decoded once, measured and thrown away. The browser is
// handed these bytes as they are, so a file cut short or corrupted inside a
// scan would otherwise be published and paint nothing.
//
// REMOVE AFTER 2027-06, with the legacy frame path in wallsocket and in
// frontend/apps/site/src/lib/wall-decode.ts: by then a camera still uploading
// JPEG has had a year of firmware that sends HEIF.
func StripJPEG(b []byte) ([]byte, int, int, error) {
func StripJPEG(b []byte) ([]byte, int, int, Luma, error) {
out, w, h, err := strip(b)
if err != nil {
return nil, 0, 0, err
return nil, 0, 0, Luma{}, err
}
if _, err := jpeg.Decode(bytes.NewReader(out)); err != nil {
return nil, 0, 0, errors.New("JPEG: does not decode: " + err.Error())
img, err := jpeg.Decode(bytes.NewReader(out))
if err != nil {
return nil, 0, 0, Luma{}, errors.New("JPEG: does not decode: " + err.Error())
}
return out, w, h, nil
return out, w, h, lumaOfImage(img), nil
}

func strip(b []byte) ([]byte, int, int, error) {
Expand Down
27 changes: 17 additions & 10 deletions service/internal/keyframe/keyframe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,9 +297,14 @@ func TestCheckDecodes(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := Check(context.Background(), bin, f); err != nil {
l, err := Check(context.Background(), bin, f)
if err != nil {
t.Errorf("hevc=%v: %v", s.hevc, err)
}
// testsrc is bars, a gradient and a counter: anything but flat.
if l.P95-l.P5 < 100 {
t.Errorf("hevc=%v: testsrc measured as %+v", s.hevc, l)
}
}
}

Expand All @@ -325,7 +330,7 @@ func TestCheckRefusesGarbage(t *testing.T) {
if _, err := NALs(bad, 4); err != nil {
t.Fatal(err)
}
if err := Check(context.Background(), bin, f); err == nil {
if _, err := Check(context.Background(), bin, f); err == nil {
t.Errorf("hevc=%v: noise decoded cleanly", s.hevc)
}
}
Expand All @@ -344,12 +349,12 @@ func TestSamples(t *testing.T) {
}
for _, p := range jpegs {
b, _ := os.ReadFile(p)
out, w, h, err := StripJPEG(b)
out, w, h, l, err := StripJPEG(b)
if err != nil {
t.Errorf("%s: %v", p, err)
continue
}
t.Logf("%s: JPEG %dx%d, %d of %d bytes kept", filepath.Base(p), w, h, len(out), len(b))
t.Logf("%s: JPEG %dx%d, %d of %d bytes kept, luma %d %d %d", filepath.Base(p), w, h, len(out), len(b), l.P5, l.P50, l.P95)
}
for _, p := range files {
b, _ := os.ReadFile(p)
Expand All @@ -358,9 +363,11 @@ func TestSamples(t *testing.T) {
t.Errorf("%s: %v", p, err)
continue
}
if err := Check(context.Background(), bin, f); err != nil {
l, err := Check(context.Background(), bin, f)
if err != nil {
t.Errorf("%s: %v", p, err)
}
t.Logf("%s: luma %d %d %d", filepath.Base(p), l.P5, l.P50, l.P95)
full, rerr := false, error(nil)
if f.HEVC {
full, rerr = FullRange(f)
Expand All @@ -386,7 +393,7 @@ func TestStripJPEG(t *testing.T) {
com = append(com, "hello"...)
tagged := append(append(append([]byte{0xFF, 0xD8}, exif...), com...), plain[2:]...)

out, w, h, err := StripJPEG(tagged)
out, w, h, _, err := StripJPEG(tagged)
if err != nil {
t.Fatal(err)
}
Expand All @@ -405,7 +412,7 @@ func TestStripJPEG(t *testing.T) {
t.Fatal(err)
}
for n := range len(tagged) / 2 {
if _, _, _, err := StripJPEG(tagged[:n]); err == nil {
if _, _, _, _, err := StripJPEG(tagged[:n]); err == nil {
t.Fatalf("accepted a JPEG cut at %d", n)
}
}
Expand Down Expand Up @@ -465,7 +472,7 @@ func TestCheckTimeoutIsItsOwnError(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := Check(context.Background(), slow, f); !errors.Is(err, ErrCheckTimeout) {
if _, err := Check(context.Background(), slow, f); !errors.Is(err, ErrCheckTimeout) {
t.Fatalf("got %v", err)
}
}
Expand All @@ -490,7 +497,7 @@ func TestStripJPEGWalksTheWholeFile(t *testing.T) {
com := append([]byte{0xFF, 0xFE}, u16(2+11)...)
com = append(com, "after scan!"...)
late := append(append(append([]byte{}, plain[:eoi]...), com...), plain[eoi:]...)
out, _, _, err := StripJPEG(late)
out, _, _, _, err := StripJPEG(late)
if err != nil {
t.Fatal(err)
}
Expand All @@ -501,7 +508,7 @@ func TestStripJPEGWalksTheWholeFile(t *testing.T) {
"cut inside the scan": plain[:eoi-40],
"no end of image": plain[:eoi],
} {
if _, _, _, err := StripJPEG(b); err == nil {
if _, _, _, _, err := StripJPEG(b); err == nil {
t.Errorf("%s: accepted", name)
}
}
Expand Down
Loading
Loading