aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--deckit/modules/k1c.py18
-rw-r--r--static/modules/k1c.js26
-rw-r--r--tests/test_k1c.py8
3 files changed, 30 insertions, 22 deletions
diff --git a/deckit/modules/k1c.py b/deckit/modules/k1c.py
index 8481e91..1f89088 100644
--- a/deckit/modules/k1c.py
+++ b/deckit/modules/k1c.py
@@ -41,10 +41,10 @@ def _http(url, data=None, timeout=2):
return json.loads(r.read())
-def _snapshot(webcams) -> str | None:
- """The first enabled webcam's snapshot URL, if it is plain http(s)."""
+def _camera(webcams) -> str | None:
+ """The first enabled webcam's live MJPEG stream URL, if it is plain http(s)."""
for cam in webcams or []:
- url = cam.get("snapshot_url") or ""
+ url = cam.get("stream_url") or ""
if cam.get("enabled", True) and url.startswith(("http://", "https://")):
return url
return None
@@ -64,7 +64,7 @@ def _first(*values):
return next((v for v in values if v is not None), None)
-def summarize(status: dict, snapshot: str | None = None) -> dict:
+def summarize(status: dict, camera: str | None = None) -> dict:
"""The panel's state from Moonraker's object status."""
ps = status.get("print_stats") or {}
sd = status.get("virtual_sdcard") or {}
@@ -99,14 +99,14 @@ def summarize(status: dict, snapshot: str | None = None) -> dict:
(status.get("output_pin fan0") or {}).get("value")),
"position": ((status.get("toolhead") or {}).get("position") or [None] * 3)[:3],
"speed_factor": (status.get("gcode_move") or {}).get("speed_factor"),
- "snapshot": snapshot,
+ "camera": camera,
}
def make(cfg: dict) -> Module:
base = str(cfg.get("k1c", {}).get("url", "")).rstrip("/")
http = cfg.get("http", _http)
- down = {"until": 0.0, "snapshot": None}
+ down = {"until": 0.0, "camera": None}
# The current file's slicer metadata, fetched once per file.
meta = {"file": None, "thumbnail": None, "estimate": None}
@@ -130,12 +130,12 @@ def make(cfg: dict) -> Module:
return {"offline": True}
try:
status = http(base + QUERY)["result"]["status"]
- if down["snapshot"] is None:
- down["snapshot"] = _snapshot(http(base + "/server/webcams/list")["result"]["webcams"]) or ""
+ if down["camera"] is None:
+ down["camera"] = _camera(http(base + "/server/webcams/list")["result"]["webcams"]) or ""
except (OSError, ValueError, KeyError, TypeError):
down["until"] = time.monotonic() + BACKOFF
return {"offline": True}
- st = summarize(status, down["snapshot"] or None)
+ st = summarize(status, down["camera"] or None)
file_meta(st["file"])
st["thumbnail"] = meta["thumbnail"]
# Until the file-position estimate settles, the slicer's is better.
diff --git a/static/modules/k1c.js b/static/modules/k1c.js
index 4cd0358..d8211fa 100644
--- a/static/modules/k1c.js
+++ b/static/modules/k1c.js
@@ -184,16 +184,24 @@ export default {
tile("Filament", st.filament ? `${(st.filament / 1000).toFixed(2)} m` : "—"),
);
- // A fresh snapshot each poll, fetched by the tablet from the printer
- // directly. ponytail: needs the tablet on the printer's LAN; proxy it
- // through the server if it ever is not.
+ // The live MJPEG stream, which an <img> plays by itself, fetched by the
+ // tablet from the printer directly. Connected only while this tab is on
+ // screen: a hidden tab would keep the printer streaming for nothing.
+ // ponytail: needs the tablet on the printer's LAN; proxy it through the
+ // server if it ever is not.
const cam = el.querySelector(".k1c-cam");
- if (st.snapshot && !cam._loading) {
- cam._loading = true;
- cam.onload = () => { cam._loading = false; cam.hidden = false; };
- cam.onerror = () => { cam._loading = false; cam.hidden = true; };
- cam.src = `${st.snapshot}${st.snapshot.includes("?") ? "&" : "?"}t=${Date.now()}`;
+ const shown = st.camera && !document.hidden && el.offsetParent !== null;
+ if (shown && cam.dataset.src !== st.camera) {
+ cam.dataset.src = st.camera;
+ // Shown at once: a never-ending stream may never fire load. Dropped
+ // (camera restarted): hide it and forget it, so the next poll reconnects.
+ cam.onerror = () => { cam.hidden = true; delete cam.dataset.src; };
+ cam.src = st.camera;
+ cam.hidden = false;
+ } else if (!shown && cam.dataset.src) {
+ delete cam.dataset.src;
+ cam.removeAttribute("src");
+ cam.hidden = true;
}
- if (!st.snapshot) cam.hidden = true;
},
};
diff --git a/tests/test_k1c.py b/tests/test_k1c.py
index 47c25ec..300a56a 100644
--- a/tests/test_k1c.py
+++ b/tests/test_k1c.py
@@ -35,7 +35,7 @@ STATUS = {
"toolhead": {"position": [10.0, 20.0, 3.2, 100.0]},
"gcode_move": {"speed_factor": 1.0},
}
-CAMS = {"result": {"webcams": [{"enabled": True, "snapshot_url": "http://printer.example.org:8080/?action=snapshot"}]}}
+CAMS = {"result": {"webcams": [{"enabled": True, "stream_url": "http://printer.example.org:8080/?action=stream"}]}}
class Fake:
@@ -65,7 +65,7 @@ class K1C(unittest.TestCase):
self.assertEqual((st["layer"], st["layers"]), (12, 80))
self.assertEqual(st["extruder"]["target"], 220.0)
self.assertEqual(st["position"], [10.0, 20.0, 3.2])
- self.assertTrue(st["snapshot"].endswith("action=snapshot"))
+ self.assertTrue(st["camera"].endswith("action=stream"))
self.assertEqual(st["thumbnail"], URL + "/server/files/gcodes/.thumbs/part-300x300.png")
def test_thumbnail_is_relative_to_the_file_and_quoted(self):
@@ -107,8 +107,8 @@ class K1C(unittest.TestCase):
with self.assertRaises(k1c.ModuleError):
m.actions["emergency_stop"]({})
- def test_snapshot_needs_http(self):
- self.assertIsNone(k1c._snapshot([{"snapshot_url": "javascript:alert(1)"}]))
+ def test_camera_needs_http(self):
+ self.assertIsNone(k1c._camera([{"stream_url": "javascript:alert(1)"}]))
if __name__ == "__main__":