diff --git a/API.md b/API.md index 97804af..1c44e9f 100644 --- a/API.md +++ b/API.md @@ -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 diff --git a/Development.md b/Development.md index 7931878..1c2d4b4 100644 --- a/Development.md +++ b/Development.md @@ -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 diff --git a/Videos.md b/Videos.md index f301226..106eb42 100644 --- a/Videos.md +++ b/Videos.md @@ -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