API, Videos: recording options, kept originals and the 4K cap
The editor recording now opens a dialog, and the server keeps what was
uploaded next to the MP4.
- Videos describes Start recording... with its options, Convert and
Import after conversion, and that Stop always uploads.
- Download gives the original, a file left unconverted shows in the
picker with a Convert button, and the cap is 3840x2160.
- API names convert=0 on upload and POST videos/{file}/convert.
- Development names 2045 e2e checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent
0598e2d27d
commit
34ae82ef8a
3 files changed
+58
-22
No files matched your search
+6
-4
@@ -28,9 +28,10 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT
|
||||
| GET | `sounds` | `[{file,name,bytes,updatedAt,owner,deletable}]` — what the sound picker offers |
|
||||
| POST | `sounds&name=jump.wav` | **raw sound bytes** → `{file,name,bytes}` (201); **422** for bytes that are no sound |
|
||||
| DELETE | `sounds/{file}` | 204, **403**, **409** like an image |
|
||||
| GET | `videos` | `[{file,name,status,…,bytes,updatedAt,owner,deletable}]` — what the video picker offers. `status` is `queued`, `converting` (with `progress`, 0 to 1), `failed` (with `error`) or `ready` (with `width`, `height`, `duration`, `fps`, `audio` and `poster`, the `.jpg` beside the MP4) |
|
||||
| POST | `videos&name=holiday.mov` | **raw video bytes** → the same record (201). The name `{folder}/{sha256}.mp4` is the hash of the upload and known at once; the worker converts it later (see [Videos](Videos)). The same bytes again answer with what is there, a failed one is tried again. **422** for bytes that cannot be a video (the start of an ISO/MP4, Matroska, AVI or MPEG-TS file), **413** above 2 GB. Unlike every other upload it is streamed to disk, so the 256 MB ceiling of the other routes does not apply |
|
||||
| DELETE | `videos/{file}` | 204, **403**, **409** like an image; also for one that waits or failed, which drops the upload |
|
||||
| GET | `videos` | `[{file,name,status,…,original,bytes,updatedAt,owner,deletable}]` — what the video picker offers. `status` is `queued`, `converting` (with `progress`, 0 to 1), `failed` (with `error`), `unconverted` (kept without converting it) or `ready` (with `width`, `height`, `duration`, `fps`, `audio` and `poster`, the `.jpg` beside the MP4). `original` is the upload as it came, `{folder}/{sha256}.source.{ext}` beside the MP4, null for a video uploaded before uploads were kept |
|
||||
| POST | `videos&name=holiday.mov[&convert=0]` | **raw video bytes** → the same record (201). The name `{folder}/{sha256}.mp4` is the hash of the upload and known at once; the upload is kept as it came, and the worker converts it later (see [Videos](Videos)). With `convert=0` it is only kept, `unconverted`. The same bytes again answer with what is there, a failed one is tried again. **422** for bytes that cannot be a video (the start of an ISO/MP4, Matroska, AVI or MPEG-TS file), **413** above 2 GB. Unlike every other upload it is streamed to disk, so the 256 MB ceiling of the other routes does not apply |
|
||||
| POST | `videos/{file}/convert` | → the record: an `unconverted` upload is queued, a failed one is tried again, anything else answers as it is. **403** for somebody else's, **404** for no such video |
|
||||
| DELETE | `videos/{file}` | 204, **403**, **409** like an image; also for one that waits, failed or is only kept. The upload as it came goes with it |
|
||||
| POST | `pixels` | **raw cel bytes** → `{file,bytes}` (201) — the pixels of one layer on one frame of [pixel art](Pixel-Art); **422** for anything that is not a cel. No listing and no delete: a cel is found by the name in the project |
|
||||
| GET | `plugins` | `[{file,bytes,updatedAt}]` — every `.js` in `data/plugins/`, sorted by file name |
|
||||
| GET | `plugins/{file}` | the module source as `text/javascript` (`nosniff`, `no-cache`); 404 for anything but a bare file name there — see [Plugins](Plugins) |
|
||||
@@ -126,7 +127,8 @@ pickers show; an upload that carries no `name` leaves the known one alone rather
|
||||
A file uploaded before names were kept simply has `""`, and reads as it always did.
|
||||
|
||||
**What the web server hands out as files** is only images, fonts, sounds, videos (with their
|
||||
posters and details) and cels. A video waiting in `data/videos/queue/` is hidden like a project. Everything else
|
||||
posters, details and the uploads as they came) and cels. The jobs in `data/videos/queue/` are
|
||||
hidden like a project. Everything else
|
||||
under `data/` (projects, snapshots, releases, accounts, sessions) is written readable for PHP alone
|
||||
(`0640`, `AtomicFile::HIDDEN`), so nginx answers **403** for it and the sign-in cannot be walked
|
||||
around. Every file the API writes goes through `AtomicFile`: a temp file beside it, its mode, then a
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ php tests/php/releases.test.php # the release stor
|
||||
php tests/php/files.test.php # uploaded files in a folder per account (#344), the same way
|
||||
php tests/php/videos.test.php # the video store, its queue and the worker (#402), with the
|
||||
# real ffmpeg: the CI image and game-maker-video have it
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 2043 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 2045 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
|
||||
./e2e.sh start [GM_STEPS] # the same run detached, see below
|
||||
./e2e.sh status # live progress, or the last run's result
|
||||
|
||||
+51
-17
@@ -11,18 +11,26 @@ the name it was uploaded under and what it is ("1920x1080 · 12.5 s · 29.97 fps
|
||||
`.avi`, told apart by the extension).
|
||||
|
||||
**An upload is converted first.** The server turns every video into one format, whatever came in:
|
||||
MP4 with H.264 and AAC, `yuv420p`, at a constant frame rate, at most 1920x1080 (1080x1920
|
||||
MP4 with H.264 and AAC, `yuv420p`, at a constant frame rate, at most 3840x2160 (2160x3840
|
||||
standing up, never made larger), with a keyframe every 15 frames, so jumping to a clip start is
|
||||
quick. A phone video recorded sideways comes out standing up. The picker shows a video on its way
|
||||
quick. Until 2026-10-12 the most was 1920x1080; a video converted before stays as it came out.
|
||||
A 4K video is heavy in a game: every frame is four times the pixels of 1080p on their way to the GPU. A phone video recorded sideways comes out standing up. The picker shows a video on its way
|
||||
as "Waiting to be converted…" or "Converting… 42 %", asks the server again every second, and
|
||||
makes the asset by itself once a video uploaded from it is ready. One that cannot be converted
|
||||
says why ("Could not be converted: …"); **Delete** takes it away, uploading it again tries again.
|
||||
Uploading the same file twice converts it once.
|
||||
|
||||
**The upload is kept as it came.** **Download** in the picker and in the Video tab saves that
|
||||
file, not the MP4, under the name it was uploaded with. A video uploaded before 2026-10-12 has
|
||||
none kept, and the Video tab saves its MP4. An editor recording can be uploaded **without
|
||||
converting it** ("Not converted – Convert makes it usable"): it can be downloaded, but no project
|
||||
can wear it until **Convert** in the picker sends it to the worker.
|
||||
|
||||
**Where it goes.** The upload is stored under the hash of its bytes in your folder, like every
|
||||
other file (see [API](API)): `data/videos/{folder}/{sha256}.mp4`, beside it the poster
|
||||
`{sha256}.jpg` and the details `{sha256}.meta.json`. The name is known the moment the upload is
|
||||
in, long before the MP4 exists. A video can be up to 2 GB. It goes from the request straight to
|
||||
`{sha256}.jpg`, the details `{sha256}.meta.json` and the upload `{sha256}.source.{ext}` (the ending
|
||||
of its name, else the one its first bytes tell). Every video so takes about twice the room it
|
||||
did. The name is known the moment the upload is in, long before the MP4 exists. A video can be up to 2 GB. It goes from the request straight to
|
||||
disk, so the API holds none of it in memory: 300 MB went up with php-fpm at 14 MB.
|
||||
|
||||
## The asset
|
||||
@@ -59,7 +67,8 @@ The clip gestures are the same as in the [Sound tab](Sounds), with the track in
|
||||
[Sounds](Sounds#master-routing)). So a recording of the editor has it.
|
||||
- The wheel zooms the time, the middle button pans, **Fit** shows the whole video. A wheel step
|
||||
and Fit are shown as the short zoom transition of the other tabs (see [Concepts](Concepts)).
|
||||
- **Download** saves the MP4 the server made, under the asset's name.
|
||||
- **Download** saves the video as it was uploaded, under the asset's name: a recording comes out
|
||||
as the `.mkv` the editor made. One uploaded before uploads were kept saves its MP4.
|
||||
|
||||
A clip's **Start (s)** and **End (s)** are in the inspector as well; an end of 0 is the end of
|
||||
the video. A clip is a frame long at least, its edges are stored to the millisecond.
|
||||
@@ -210,15 +219,34 @@ below shares.
|
||||
|
||||
## Recording the editor
|
||||
|
||||
**Start recording** in the main menu (top left) records the whole editor, for a tutorial or a bug
|
||||
**Start recording…** in the main menu (top left) records the whole editor, for a tutorial or a bug
|
||||
report. While it records, the entry reads **Stop recording**, and a red dot with the time stands
|
||||
beside the menu button.
|
||||
|
||||
- **No options:** the name is `Recording <date> <time>`, the size is the editor's canvas in device
|
||||
pixels (an odd row or column is cut off), 60 fps. The sound is the master, as for the game view.
|
||||
- **The mouse pointer is drawn in.** The system draws it over the page, so it is not in the canvas;
|
||||
the recording draws its own in the shape the editor shows (arrow, hand, resize arrows, I-beam,
|
||||
crosshair).
|
||||
The dialog it opens starts on the last choice made in this browser (the name excepted):
|
||||
|
||||
| Option | |
|
||||
|---|---|
|
||||
| **Name** | empty: `Recording <date> <time>` |
|
||||
| **Resolution** | *Screen*: the canvas in device pixels. *1080p*, *1440p*, *4K*: fitted into 1920x1080, 2560x1440 or 3840x2160 with the window's shape. The line under it says the size this window comes out at |
|
||||
| **FPS** | 60 or 30 |
|
||||
| **Mouse pointer** | drawn in or not |
|
||||
| **Sound** | the master, as for the game view, or none |
|
||||
| **Convert** | the server makes an MP4 of it, which a project can wear. Off: it is only kept |
|
||||
| **Import after conversion** | the MP4 becomes an asset of the project once it is ready. Grey while Convert is off |
|
||||
|
||||
The recording is always uploaded; **Download** in the video picker saves it as it was recorded.
|
||||
|
||||
- **A larger frame is drawn for, not scaled up to.** For 4K the editor draws finer while it
|
||||
records (from a 1920x1080 window twice as fine), so text and lines stay sharp; the screen shows
|
||||
the same as before. A window too small for its frame even at four times as fine is scaled up the
|
||||
rest of the way. A smaller frame keeps the screen's sharpness and is scaled down. Exactly
|
||||
3840x2160 needs a window of that shape: open the editor with `?w=1920&h=1080`.
|
||||
- **Cost:** 4K at 60 fps is about 40 Mbit/s, so the 2 GB a file may have last about 7 minutes. The
|
||||
encoder is whatever Chrome has; frames it cannot keep up with are dropped. 30 fps halves both.
|
||||
- **The mouse pointer** is drawn in when asked: the system draws it over the page, so it is not in
|
||||
the canvas; the recording draws its own in the shape the editor shows (arrow, hand, resize
|
||||
arrows, I-beam, crosshair).
|
||||
- **The red dot is not in it.** It is an HTML element over the canvas, the one thing on the page
|
||||
outside it, so the copy of the canvas never holds it; the room it needs beside the menu stays
|
||||
empty in the recording. Clicks go through it.
|
||||
@@ -229,16 +257,21 @@ beside the menu button.
|
||||
- **A resized window** is fitted into the size the recording started with, centred, the rest dark.
|
||||
- **A size of its own:** open the editor with `?w=1920&h=1080` and its canvas is laid out at that
|
||||
size, whatever the window. A smaller window shows it shrunk to fit, a larger one centred at full
|
||||
size. The recording is that size times the screen's scaling (up to 2): at 100 % exactly 1920*1080.
|
||||
size. With *Screen* the recording is that size times the screen's scaling (up to 2): at 100 %
|
||||
exactly 1920*1080.
|
||||
- It runs on through Play and Stop, beside a recording of the game view if a game starts one.
|
||||
Saving is the same: up to the server, then a video asset once the editor is not playing.
|
||||
|
||||
The code is `src/editor/EditorRecorder.ts`, the pointer shapes are `src/core/recording/cursors.ts`.
|
||||
The code is `src/editor/EditorRecorder.ts` with its dialog `src/editor/EditorRecordingDialog.ts`;
|
||||
the sizes and the options are plain functions in `src/core/recording/editorRecording.ts`, the
|
||||
pointer shapes are `src/core/recording/cursors.ts`.
|
||||
|
||||
## The worker
|
||||
|
||||
Converting is a job, not a request. The API puts the upload into `data/videos/queue/` (hidden
|
||||
from the web server), and a worker takes one after the other, oldest first:
|
||||
Converting is a job, not a request. The API keeps the upload beside where the MP4 will be and puts
|
||||
a job for it into `data/videos/queue/` (hidden from the web server), and a worker takes one after
|
||||
the other, oldest first. A failed job leaves the upload where it is, so it can still be downloaded.
|
||||
A job from before uploads were kept has its bytes hidden in the queue, and they go once it is done:
|
||||
|
||||
- On stein2 that is the container **`game-maker-video`**: PHP and ffmpeg on Debian 12, the
|
||||
checkout mounted, running as `www-data` without a network, on at most 3 of the 4 cores. It is
|
||||
@@ -250,5 +283,6 @@ from the web server), and a worker takes one after the other, oldest first:
|
||||
|
||||
The worker is `tools/video-worker.php` over `api/lib/VideoWorker.php`; the rules (size, frame
|
||||
rate, the ffmpeg arguments, the progress) are plain functions in `api/lib/VideoFormat.php`. A job
|
||||
cut off by a stopped worker is started again when it comes back. Measured on stein2: 5 s of
|
||||
2560x1440 at 60 fps came out as 1080p60 in 5.0 s.
|
||||
cut off by a stopped worker is started again when it comes back. Measured on stein2, still with
|
||||
the 1080p ceiling: 5 s of 2560x1440 at 60 fps came out as 1080p60 in 5.0 s. A 4K video takes
|
||||
longer; that is not measured.
|
||||
Reference in new issue
Block a user