Files
forgefirm/docs/VIDEO.md
T
ScottW514 a47c0c7f6d Video offload: Mesa etnaviv in the image, forgectrl pinned, the H.264 stream in the catalog
The stream conversion now runs on the GC880 when the image carries the
GL stack, and /cam/h264 serves the same picture as fragmented MP4 in a
fraction of MJPEG's bytes (forgectrl 6573abd). This side supplies what
that needs and holds it to account:

- Distro: opengl stays a feature, with mesa trimmed to exactly the
  path used (gallium etnaviv, GLES/EGL/GBM, no GLX, no display
  platforms). Image: libegl-mesa, libgles2-mesa, libgbm,
  mesa-megadriver. forgectrl dlopens them, so an image without them
  still streams on NEON. Platform change: the next campaign is full.
- forgetest: camera.h264-stream exercises the endpoint end to end
  (codec header, init segment, media fragments, /cam/status encoder
  state) and accepts a clean 503 on a machine without the stack;
  the camera covers name the new forgectrl files.
- VIDEO.md 5.6 now describes both streams and why H.264 earns its
  place (the WiFi transmit path measures about 7 percent of the core
  per MB/s sent); BRINGUP records the bench-validation checklist as
  Next work item 20, including the release-size watch: mesa must fit
  the 200 MiB slot gate.
2026-08-24 08:10:33 -04:00

441 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Video and the cameras
The machine has two cameras — one in the lid looking down at the bed, one in the
print head looking at the material under the lens — and ForgeFIRM serves both
over plain HTTP from the web control panel: **MJPEG** for anything that can read
a stream of JPEGs, and an **H.264** live stream for clients that decode video
(the panel uses it when the browser can). There is no app, no cloud relay, and
no proprietary protocol.
One rule governs all of it: **the cameras only capture with the lid closed**
(§2). Everything else here assumes that condition is met.
This page explains what you get, how to point a client at it, what the sensors
are physically capable of versus what ForgeFIRM actually sends and why, and what
you can change.
- For sender setup in general, see [Connecting LightBurn](LIGHTBURN.md).
- The cloud mode described in §8 is documented in
[Cloud mode](https://github.com/ScottW514/python3-gfhardware/blob/master/forgefirm-app/docs/CLOUD.md).
---
## 1. What the hardware is
The two cameras carry the same kind of sensor and feed one shared path into the
board. A **hardware MIPI switch** (the factory `CAM_SEL` line) selects which of
them reaches the board's single camera receiver, so **exactly one camera can be
capturing at any moment**. That is a property of the board, not a software
limit — there is no configuration in which both stream at once.
| | Lid camera | Head camera |
|---|---|---|
| Sees | the whole bed, from above | the material directly under the lens |
| Its lamp | the lid LED strip | the white LED in the print head |
| Used for | bed view, camera-referenced homing, LightBurn's camera overlay | focus and material inspection (cloud mode's distance measurement) |
The lid lens is a wide fisheye, which matters for how you use the image (§5.5).
Which sensor is fitted depends on the machine. Standard machines carry a **5 MP
OV5648**; "HD" machines carry an **8 MP OV8856**. ForgeFIRM reads which one
bound and configures itself accordingly — one firmware image covers both — and
reports it in `/cam/status` and on the panel's Status tab.
---
## 2. Privacy: the cameras only work with the lid closed
**Neither camera captures anything while the lid is open.** Not the live view,
not a snapshot, and not an image requested by the Glowforge service in cloud
mode. Close the lid and everything works; open it and the sensors stop.
The reason is where the lid camera points. It is mounted in the lid, so raising
the lid swings it up to face the room — and in cloud mode the shutter is not
yours to press: the service asks for images on its own schedule, whenever it is
connected. The rule removes the question. The enclosure being shut is the
condition for an image to exist at all.
**What the rule covers**
- **Both cameras.** The head camera is gated too, so this is one rule to
remember rather than a rule with an exception you have to trust.
- **Every way in:** the panel, `/cam/stream`, `/cam/snapshot`, the
mjpg-streamer aliases, LightBurn, and cloud mode's image actions.
- **Capture already running.** Opening the lid stops a live stream within
about a frame and shuts the sensor down; it does not merely block new
requests.
- **The lamps.** A refused capture never raises them, so an attempt with the
lid open leaves no trace.
**How it behaves**
| Situation | What happens |
|---|---|
| Snapshot requested with the lid open | `409` and a message naming the lid; no image data |
| Stream requested with the lid open | `409`; the stream never opens |
| Lid opened while a stream is running | the stream ends cleanly and the pipeline is torn down |
| Lid state unreadable | treated as open — capture refused |
| Cloud service asks for an image with the lid open | refused, and reported back to the service as a failed action rather than left hanging |
| Lid closed again | everything works immediately; nothing to restart |
`GET /cam/status` reports it: **`capture_allowed`** is false whenever the lid is
open, and **`stopped_by_lid`** records that the last capture ended because the
lid opened rather than going idle. The panel's Status tab says *lid open — the
cameras are off* rather than showing a stream error.
**Where the check comes from.** The lid signal is the same one the hardware
safety chain uses to gate the beam — the series combination of both lid
switches, not a software flag — and the check **fails closed**: if the lid
state cannot be read at all, the cameras stay dark. That direction is proven by
a unit test in CI; the end-to-end behavior is an acceptance test run on real
hardware.
**One thing it costs.** The factory firmware ran the cloud's focus *hunt* with
the lid open, and part of a hunt is a head capture. Those captures are now
refused, so a hunt attempted with the lid open fails instead of completing.
Close the lid before letting the app focus or print.
**What it is not.** This is a rule enforced by the two programs that own the
sensors, not a hardware cut-off: the sensor rails stay powered, and anyone with
root on the machine could bypass it. It protects you from the Glowforge
service, from other software on your network, and from a stream you forgot was
running — not from someone who already controls the board. There is
deliberately no setting to turn it off.
---
## 3. Watching it
**In the panel.** Open `http://<machine>:8080/` and go to the **Status** tab.
The *Lid camera* card shows a still by default with **Live** and **Refresh**
buttons; **Live** switches the same frame to the running stream.
**From another program.** The endpoints are:
| URL | What it returns |
|---|---|
| `/cam/stream?cam=lid` | continuous MJPEG (`multipart/x-mixed-replace`) |
| `/cam/stream?cam=head` | the same, from the head camera |
| `/cam/h264?cam=lid` | continuous H.264 as fragmented MP4 (see §5.6): the same picture in a fraction of the bytes, for clients that decode video |
| `/cam/snapshot?cam=lid` | one full-resolution JPEG |
| `/cam/snapshot?cam=lid&res=half` | one half-resolution JPEG (much faster) |
| `/cam/status` | JSON: which sensor, which camera, frame rate, frame sizes, whether the lid currently permits capture |
| `/?action=stream` | the lid stream again, under the name mjpg-streamer clients expect |
| `/?action=snapshot` | one full-resolution lid JPEG, same aliasing |
The `?action=` pair exists because a lot of software — print-server dashboards,
camera widgets, anything written against mjpg-streamer — assumes those exact
URLs. Point such a client at `http://<machine>:8080/` and it will work.
Every one of them answers **`409`** with the lid open (§2), so a client that
checks status codes can tell "close the lid" apart from "the camera is broken".
**Access.** Reading the camera needs no token. It does need a request that
addresses the machine by IP address (or `localhost`) and, from a browser, one
that is not cross-site — that is what stops a hostile page in another tab from
reaching into your machine. It is not protection against other people on your
LAN. Anything that *changes* machine state does need the panel's token. In
practice: paste the URL into any local client and it works.
---
## 4. What you actually get
| | 5 MP machine (OV5648) | 8 MP machine (OV8856) |
|---|---|---|
| Sensor frame captured | 2592 × 1944 | 3264 × 2448 |
| Live stream | 1296 × 972 | 1632 × 1224 |
| Full snapshot | 2592 × 1944 | 3264 × 2448 |
| Half snapshot | 1296 × 972 | 1632 × 1224 |
| Stream formats | MJPEG (quality 75 by default) and H.264 (~1.5 Mbit/s by default) | same |
| Frame rate | **15 fps** sustained | not yet measured (§10) |
Measured on a 5 MP machine: 15.0 fps with a viewer attached, which is the rate
the sensor itself produces in this mode — the machine is not the bottleneck.
The daemon uses about 41 % of one CPU with one viewer, and LightBurn can watch
the stream while jogging from the same session without disturbing motion. A
full-resolution still takes about 2.4 s to produce (2.7 s if the camera has to
be started first), because 5 megapixels of demosaicing and JPEG encoding happen
on the machine's CPU.
The stream frame is not a resampled copy of the full frame. Each 2 × 2 group of
sensor pixels becomes exactly one output pixel, which is why the stream is
precisely half the capture in each axis and why it is cheap enough to run
continuously.
The 41 % figure is the NEON demosaic feeding the hardware JPEG encoder. When
the GPU demosaic and the H.264 stream carry the load instead (§5.6), the
stream's CPU cost drops to bookkeeping; those two paths are newer than the
figure above and their own numbers will be measured on the bench the same way.
---
## 5. What the sensor can do versus what ForgeFIRM sends
This is where expectations usually come unstuck: the sensors are more capable
on paper than the video you get. Each difference below is deliberate and has a
reason.
| | The sensor can | ForgeFIRM sends | Why |
|---|---|---|---|
| Live resolution | full frame | half in each axis | CPU and bandwidth; §5.1 |
| Frame rate (5 MP) | 30 fps in reduced modes | 15 fps | the full-field mode runs at 15 fps; §5.1 |
| Resolution (8 MP) | 3280 × 2464 | 3264 × 2448 | the widest frame the board's camera receiver can take; §5.2 |
| Bit depth | 10 bits per pixel | 8 bits | JPEG is 8-bit, and 8-bit is what makes §5.2 fit |
| Exposure / color | auto exposure and auto white balance | fixed values | a bed image has to look the same frame to frame; §5.4 |
| Lens | — | no correction applied | correction belongs in the client; §5.5 |
| Encoding | — | MJPEG and H.264, nothing recorded | §5.6 |
| Mirroring | a mirror register | mirrored in software instead | the register breaks capture on this board; §5.7 |
### 5.1 Resolution and frame rate on a 5 MP machine
The OV5648 offers several modes, and they are not simply "the same picture,
smaller":
| Mode | Rate | Field of view |
|---|---|---|
| 2592 × 1944 | 15 fps | the whole sensor |
| 1920 × 1080 | 15 fps | a crop from the middle |
| 1600 × 1200 | 15 fps | a crop from the middle |
| 1280 × 960 | 30 fps | the whole sensor, every other pixel |
| 1280 × 720 | 30 fps | a crop, every other pixel |
| 640 × 480 | 30 fps | the whole sensor, every fourth pixel |
ForgeFIRM runs the **2592 × 1944** mode. The cropped modes are unusable for a
bed camera — they would show the middle of the bed and cut off the corners. That
leaves the full-frame mode at 15 fps or the skipped 1280 × 960 mode at 30 fps.
The camera runs **one mode at a time**, and the live stream and the snapshots
come from the same frames: that is what lets a snapshot be delivered while a
stream is running, and it is why the picture does not stutter or re-expose when
you take one. Choosing 1280 × 960 would double the frame rate and permanently
give up full-resolution stills — and full resolution is exactly what bed
alignment, camera calibration and cloud mode need. Full stills win; 15 fps is
the price.
The stream is halved to 1296 × 972 rather than sent at full size because
demosaicing and encoding 5 megapixels 15 times a second is far beyond this
CPU, and because a 5 MP live view of the bed is of no practical use — it is a
positioning aid, not a photograph.
### 5.2 8 MP ("HD") machines: a few rows short of the full array
The OV8856's largest frame is 3280 × 2464. ForgeFIRM captures **3264 × 2448**,
which is 16 columns and 16 rows less — the whole field of view, edge to edge,
just without the last few pixels of margin.
Getting there is not free, and it explains why §5.3 matters. The sensor can
send its full frame over the two data lanes this board wires, but at 10 bits
per pixel that means running the link at **1.44 Gbit/s per lane**, and the
i.MX6's camera receiver tops out at **1 Gbit/s per lane** — it has no timing
setting for anything faster, so it refuses the mode outright. Asking the sensor
for 8-bit pixels instead cuts a fifth off every sample and lets the same frame
travel at half the rate, which the receiver takes comfortably. That is how an
HD machine gets its full resolution, and it costs nothing, because the
delivered JPEG was going to be 8-bit anyway.
The result is about 15 frames per second off the sensor, and roughly the same
bytes per second across the bus as a 5 MP machine at its own full frame.
### 5.3 Ten bits in, eight bits out
Both sensors can emit 10 bits per pixel. ForgeFIRM asks both for 8 instead, and
the delivered image is 8 bits per channel because that is what JPEG is.
Two bits would buy nothing without a tone curve to spend them on, and there is
no tone curve (§5.4) — while asking for 8 halves the data crossing the bus,
which is what keeps the stream cheap on a 5 MP machine and what makes full
resolution reachable at all on an 8 MP one (§5.2).
### 5.4 Exposure, gain and color are fixed
There is no auto-exposure and no auto white balance. Exposure, gain and the
color balance are set to fixed values when the camera starts, matching what the
factory firmware used, and they are not adjustable from the panel.
That is deliberate. A bed camera is a measuring instrument: camera-referenced
homing, LightBurn's overlay, and cloud mode's alignment all compare images to
known geometry, and an image whose brightness and color shift between frames —
as the head moves through the frame, or as the laser flashes — is worse than a
consistently imperfect one.
ForgeFIRM also applies **no gamma, tone curve, sharpening or noise reduction**.
The JPEG is the sensor's data, demosaiced and encoded. Compared with a phone
photo the result looks flat. That is expected; it is not a fault, and it does
not affect how well the image works for alignment.
One consequence on 8 MP machines: that sensor's driver publishes no color
balance controls at all, so those images will be less color-correct than a
5 MP machine's until the exposure and gain are commissioned on real hardware.
### 5.5 The lens is not corrected
The lid lens is a wide fisheye and the image has heavy barrel distortion —
straight bed edges bow. ForgeFIRM sends the image as the lens sees it and does
not attempt to flatten it.
Correction belongs where the calibration lives: **LightBurn's camera
calibration** pass measures your particular machine's lens and applies the
correction on the host, which is more accurate than a fixed correction baked
into the firmware and costs the machine's CPU nothing. Run that calibration
before trusting the camera overlay for placement.
### 5.6 Two streams, one picture: MJPEG and H.264. Nothing is recorded.
The same live picture is served two ways, and **the machine never writes video
to disk**.
**MJPEG** (`/cam/stream`) is the universal one: every frame is a complete
JPEG, so a viewer can join or leave at any moment, a dropped frame costs
nothing, and browsers, LightBurn and mjpg-streamer clients consume it with no
plugin. It stays, unchanged, and it is what anything that cannot decode video
should use.
**H.264** (`/cam/h264`) exists because bytes on this machine are not free.
MJPEG re-sends the whole scene fifteen times a second, roughly 9 Mbit/s, and
the WiFi transmit path runs on the machine's single CPU core, where measured
cost is about 7 % of the core per MB/s sent. A bed camera's scene barely
changes between frames, which is exactly what an inter-frame codec exploits:
the H.264 stream carries the same picture in roughly 1.5 Mbit/s and gives most
of that CPU back. It arrives as fragmented MP4, the form a browser's Media
Source Extensions accept, with the codec named in an `X-H264-Codec` response
header; the panel's **Live** button uses it automatically where the browser
can and falls back to MJPEG where it cannot. Latency is a beat behind MJPEG
(under a second), which is why LightBurn keeps consuming the MJPEG stream.
Both encoders are hardware: JPEG frames come from the CODA960's JPEG unit and
H.264 from its BIT processor, two independent engines, so serving both at once
does not double any cost that matters. The demosaic that feeds them runs as
fragment shaders on the SoC's GC880 GPU when the image ships the GL stack
(reported as `"convert": "gpu"` in `/cam/status`), reading the sensor frame
and writing the encoder's buffer directly, so a stream frame never crosses the
CPU at all; without the GPU it falls back to the NEON demosaic. (Stills are
still demosaiced and encoded on the CPU, which is most of why a
full-resolution one takes a couple of seconds.)
One more consumer of nothing: with a frame-rate cap set (`FORGECTRL_STREAM_FPS`
of 1 or more), the cap is programmed into the CSI receiver's frame-skip
hardware, and skipped frames are dropped before they are ever written to
memory. `/cam/status` reports `"hw_fps_skip": true` when that is in effect.
If you want a recording, record the stream on the computer watching it. The
machine stores its firmware, settings and logs on a small internal flash device
and has no recording feature to fill it with.
### 5.7 The mirror is applied in software
The image is mirrored horizontally to match the orientation the factory
software produced. The sensors have a mirror register that would do this for
free, but setting it breaks the board's capture path — frames stop completing
altogether — so the flip is done while the image is being demosaiced instead.
The cost is negligible and the result is identical.
---
## 6. Lighting
Each camera has its own lamp, and ForgeFIRM drives them around captures:
- **While capturing**, the relevant lamp is raised to a fixed working level and
restored when the camera goes idle.
- **At rest**, the lid lamp sits at the `lid_lamp_idle` setting (0–255, default
236) — the bed light you normally see. It is asserted when the daemon starts,
when you change the setting, and whenever a controller starts.
- **Per shot**, `/cam/snapshot` accepts `lamp=0..1023` to override the level for
that one image; a few frames are discarded afterward so the image you get was
actually exposed under the light you asked for.
In cloud mode the cloud client drives the lid lamp for as long as it runs, and
ForgeFIRM re-asserts your idle level the next time a controller starts.
---
## 7. Sharing one camera path
Because only one camera can capture at a time, requests have to be arbitrated.
The rule is **the newest request wins**, on the assumption that one person is
standing at the machine:
- **A new stream preempts the current one.** Existing viewers' streams end
cleanly — their picture freezes on the last frame — rather than being torn
mid-frame.
- **A snapshot of the other camera borrows the path.** The stream pauses, the
mux switches, one frame is taken, and it switches back; viewers see a gap of
a second or two. Snapshots do not fail because someone else is watching.
- **The camera shuts down after 10 seconds** with nobody watching and no
snapshot pending, so other software on the machine can use it — and
immediately, whoever is watching, if the lid opens (§2).
---
## 8. Who else uses the cameras
- **Camera-referenced homing** (`$H`) takes a lid image and has the Glowforge
service work out where the head is. This is the factory homing method, so it
needs a service session; it is the one part of GRBL mode that does.
- **Cloud mode** captures both cameras on demand through the same snapshot
endpoint, so it obeys the same arbitration as everything else.
- **LightBurn** consumes the lid stream for its camera overlay while it drives
motion over the Grbl connection; the two coexist.
---
## 9. When something looks wrong
**No picture at all, and a 409 mentioning the lid.** Working as intended: the
lid is open (§2). Close it. `/cam/status` shows `capture_allowed: false` while
that is the case. If the lid *is* shut and you still see this, one of the two
lid switches is not making — the same condition that would stop the laser
firing, so it is worth investigating rather than working around.
**A black or nearly black picture.** The scene is not lit: the exposure is
fixed, so the camera cannot compensate. Check `lid_lamp_idle`, and remember
snapshots can carry their own `lamp=` level.
**The stream stops on its own.** Either the lid opened (§2 — the panel says
so), or someone else — another browser tab, LightBurn, the panel — asked for
the other camera, or for a stream, and preempted yours. Reload; the panel does
this automatically and says which it was.
**"camera switch timed out".** A viewer would not let go within the grace
period. Close the other viewer and retry.
**A snapshot returns 503.** The camera could not start. The usual cause is
another process holding the capture device; the daemon's log names the failing
step.
**`/cam/status` reports `"sensor": "unknown"`.** No camera was found on that
bus, or a sensor bound that this firmware has no profile for. On an HD machine
see §9.
**The picture is bowed / placement is off.** Expected without calibration; see
§5.5.
---
## 10. Status of 8 MP ("HD") machines
Everything an 8 MP machine needs is in the firmware: the kernel patches for the
OV8856 — including the 8-bit full-resolution mode §5.2 depends on — the
device-tree entries, and a capture path that picks its geometry and sensor
controls from whichever sensor bound.
**None of it has run on an 8 MP machine.** No such unit has been available to
test against, so treat 8 MP support as untested rather than working: whether
the receiver locks onto the full-resolution mode, and what exposure and gain
the sensor actually wants, can only be settled on that hardware. Frame-rate and
CPU figures in §4 are from a 5 MP machine and do not carry over — an HD machine
demosaics 60 % more pixels per frame. Reports from anyone with an HD machine
are welcome.
The 5 MP path is hardware-validated and in daily use.
---
## See also
- [Connecting LightBurn](LIGHTBURN.md) — sender setup and the camera overlay
- [Motion and laser drive](MOTION.md) — what the machine does while you watch
- [Cooling and airflow](COOLING.md)
- [Laser safety](SAFETY.md)