Skip to content
SlashClone

Canvas frame capture

@slashclone/canvas-frame-capture

Check a canvas or WebGL animation frame by frame when the browser cannot take screenshots (a hidden pane, a background tab, a headless run). Captures timed frames from the page itself and saves them as small images you can look at. Writes only to a frames folder you choose.

SkillDevOpsv0.1.0 (latest)
Activity
New: be the first to try it
  1. 1"""Save frames sent from a page as images. Standard library only.
  2. 2
  3. 3Listens on 127.0.0.1 only. Each query parameter of a GET request is one frame: its
  4. 4name becomes the file name and its value is a data URL (data:image/jpeg;base64,...).
  5. 5Usage: python3 -I scripts/frame-receiver.py [port] [out_dir] (defaults: 8766 frames)
  6. 6Stop it with Ctrl-C when the frames are saved.
  7. 7"""
  8. 8
  9. 9import base64
  10. 10import http.server
  11. 11import os
  12. 12import re
  13. 13import sys
  14. 14import urllib.parse
  15. 16PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8766
  16. 17OUT = sys.argv[2] if len(sys.argv) > 2 else "frames"
  17. 18SAFE = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
  18. 19os.makedirs(OUT, exist_ok=True)
  19. 22class Receiver(http.server.BaseHTTPRequestHandler):
  20. 23 def handle_one_request(self):
  21. 24 # The default limit is 64 KB per request line; several frames need more.
  22. 25 self.raw_requestline = self.rfile.readline(8_000_000)
  23. 26 if not self.raw_requestline or not self.parse_request():
  24. 27 self.close_connection = True
  25. 28 return
  26. 29 if self.command == "GET":
  27. 30 self.do_GET()
  28. 31 self.wfile.flush()
  29. 32 self.close_connection = True
  30. 34 def do_GET(self):
  31. 35 query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
  32. 36 saved = []
  33. 37 for name, values in query.items():
  34. 38 value = values[0]
  35. 39 if not SAFE.match(name) or not value.startswith("data:image/"):
  36. 40 continue
  37. 41 with open(os.path.join(OUT, name + ".jpg"), "wb") as f:
  38. 42 f.write(base64.b64decode(value.split(",", 1)[1]))
  39. 43 saved.append(name)
  40. 44 self.send_response(200)
  41. 45 self.send_header("Content-Type", "text/plain")
  42. 46 self.end_headers()
  43. 47 self.wfile.write(("saved " + ", ".join(saved)).encode())
  44. 48 print("saved", ", ".join(saved) or "nothing", flush=True)
  45. 50 def log_message(self, *args):
  46. 51 pass
  47. 54print(f"Waiting for frames on http://127.0.0.1:{PORT}/ (saving to {OUT}/)", flush=True)
  48. 55http.server.HTTPServer(("127.0.0.1", PORT), Receiver).serve_forever()