Videos: a new page for the video asset, its tab and worker (#402)
- Videos: getting a video in, the conversion, the asset, the Video tab, wearing it on a sprite, and the worker container. - API: the videos routes and what a record says on its way. - Architecture: the new files on both sides. - Development: the PHP test, ffmpeg in the CI image, 1994 checks. - Sidebar: Videos after Fonts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent
bfb39d12a0
commit
8f4f4af793
5 files changed
+121
-11
No files matched your search
+6
-2
@@ -28,6 +28,9 @@ 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 |
|
||||
| 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) |
|
||||
@@ -49,7 +52,7 @@ is still the price of admission. See [Accounts](Accounts).
|
||||
|
||||
## Uploaded files belong to an account (#344)
|
||||
|
||||
Every account uploads images, fonts and sounds into a **folder of its own**, and the folder is part
|
||||
Every account uploads images, fonts, sounds and videos into a **folder of its own**, and the folder is part
|
||||
of the name the project stores: `{folder}/{sha256}.{ext}`, for example
|
||||
`data/sounds/a590cab0f8b7/f1f2…f558.wav`. The folder is a random 12-character name kept as
|
||||
`folder` in `users.json`, given with the first upload. It is not the account name, so renaming an
|
||||
@@ -122,7 +125,8 @@ the server strips directories, control characters and anything not valid UTF-8,
|
||||
pickers show; an upload that carries no `name` leaves the known one alone rather than erasing it.
|
||||
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 and cels. Everything else
|
||||
**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
|
||||
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
|
||||
|
||||
+17
-6
@@ -128,7 +128,7 @@ web/index.html Vite source page (dev) index.html built page
|
||||
web/play.html a released game (#266) play.html built page (generated, served)
|
||||
src/main.ts, play.ts the two entries; page.ts is the canvas both are (the WebGL context, root, resize)
|
||||
src/core/model/ Project (object holders), Asset + AssetTypes, SceneObject (and what a list calls one:
|
||||
`objectName`, `groupName`), Properties (value types, and the walks over a nested value: `mapNested`, a struct field taken out or renamed), hierarchy, walk (generic parent/child walks), instance (container content resolved from its template), usages (where an asset is used), container (the cycle guard), placement (what may be dropped where), texture (frames, the slice grid, a frame's collision outlines, and the nine-slice and sort line of any picture, #308), cutTarget (which picture a nine-slice or sort line is set on: a texture frame, or pixel art and vector art as a whole, #308), frameOutline (#193: a frame's outlines as a collider wears them, centred, a pixel to a unit, cut into pieces once per drawn list), traceCollision (#161: a frame's outlines read off its picture — the pixels from an alpha threshold on are solid, each 4-connected island is walked round along the pixel edges and cut down to a few corners without ever crossing itself; crumbs below 1 % of the frame drop out), generated (pictures that are drawn rather than uploaded, #185: the key `gen:<assetId>@<revision>`, the register of readers that tell the picture path an asset's canvas size and revision, and the `fingerprint` pixel art and vector art make their revision with), vectorart (#129: the canvas as a property, which ring is worth drawing, and the revision as a fingerprint of the drawing rather than a counter, a texture fill's picture included) + vectorPath (what a shape draws, as plain commands: closed rings fill, every ring strokes, a segment is a line while neither of its ends carries a handle; a paint resolved to sorted stops or the picture it names) + vectorPaint (#186: gradients and texture fills — where a paint lies when it is switched on, the colour between two stops, adding, moving and removing a stop, a paint moved with its shape, and the rule that a texture fill takes uploaded pictures only) + vectorBoolean (#187: union, subtract, intersect and exclude on the sampled outlines, each shape read by the even-odd rule; the result rings in the order Pixi's hole detection needs, every outline followed by its holes and preceded by the islands inside them) + vectorCurves (#208: every sampled point remembers its cubic and its `t`, so a run of result points along one cubic becomes that cubic again, cut with de Casteljau), pixelart (#130: the palette a picture is painted in, how the layers of a frame come together, the sheet of all frames, a frame id read as a cut of it, and the revision) + pixelCel (the file of one layer on one frame, and its SHA-256 name worked out in the browser) + pixelDraw (what the tools do to the indices: brush, line, rectangle, ellipse, fill, resize about an anchor, merge, a palette colour taken out) + pixelEdit (every edit of layers, frames and cels as a function from one state to the next), polygon (an outline checked, cut into convex pieces and clipped to a rectangle), tile (the three kinds and their block shapes, reading a tile's pictures, the animation step), autotile (which quarter tile a neighbourhood asks for, the 47/16 tiles a block produces, 1 for a plain tile), tilemap (the cells of a layer, which neighbours a cell is joined to, the cells a stroke or a rectangle covers), tilemapCollision (the solid cells of a map, drawn in or out by their tile's collision scale and merged into rectangles, one group per one-way angle and per surface (`tileMaterials`, #193), the outlined cells as polygons — an autotile's outlines cut into its quarters and pieced together per cell like its picture —, where one sits in the world, and which cells of a placed map a world rectangle can reach, in cells (`cellsWithin`, #184)), tilemapMemo (what a map's cells work out to, kept while they hold the same: a version per cell map compared cell by cell, since strokes paint in place), animation (which frame at time t, a row's value there, what a row may aim at, the players on an object; KeyRef and its lookups) + animationPreview (the root at a frame, written into copies), curve (#247: a drawn easing evaluated at t — each segment a cubic solved for t by bisection, held to its rules by `sanitizedCurve`: ends at 0 and 1, no point overtaking another, no handle past its neighbour — and which keys and `Lerp` nodes use one) + curvePresets (the built-in eases traced as curves, a segment halved only where it strays), graphLookup (what a graph sees of the project: behaviors to run, state machines to read, curves to ease along), structTypes (#253: Struct assets as types — every typed struct variable, struct field and `Pack`/`Expand` found and brought in line with its asset, a renamed field carried through every value first, the cycle rule), Scope (how long a shared value lives: per instance, one for the run, or one per scene and rebuilt with every scene that loads, #164 — for a state machine's current state and for the values of a Variables asset), collisionLayers (which colliders may meet: the project's named Line truncated
|
||||
`objectName`, `groupName`), Properties (value types, and the walks over a nested value: `mapNested`, a struct field taken out or renamed), hierarchy, walk (generic parent/child walks), instance (container content resolved from its template), usages (where an asset is used), container (the cycle guard), placement (what may be dropped where), texture (frames, the slice grid, a frame's collision outlines, and the nine-slice and sort line of any picture, #308), cutTarget (which picture a nine-slice or sort line is set on: a texture frame, or pixel art and vector art as a whole, #308), frameOutline (#193: a frame's outlines as a collider wears them, centred, a pixel to a unit, cut into pieces once per drawn list), traceCollision (#161: a frame's outlines read off its picture — the pixels from an alpha threshold on are solid, each 4-connected island is walked round along the pixel edges and cut down to a few corners without ever crossing itself; crumbs below 1 % of the frame drop out), generated (pictures that are drawn rather than uploaded, #185: the key `gen:<assetId>@<revision>`, the register of readers that tell the picture path an asset's canvas size and revision, and the `fingerprint` pixel art and vector art make their revision with), vectorart (#129: the canvas as a property, which ring is worth drawing, and the revision as a fingerprint of the drawing rather than a counter, a texture fill's picture included) + vectorPath (what a shape draws, as plain commands: closed rings fill, every ring strokes, a segment is a line while neither of its ends carries a handle; a paint resolved to sorted stops or the picture it names) + vectorPaint (#186: gradients and texture fills — where a paint lies when it is switched on, the colour between two stops, adding, moving and removing a stop, a paint moved with its shape, and the rule that a texture fill takes uploaded pictures only) + vectorBoolean (#187: union, subtract, intersect and exclude on the sampled outlines, each shape read by the even-odd rule; the result rings in the order Pixi's hole detection needs, every outline followed by its holes and preceded by the islands inside them) + vectorCurves (#208: every sampled point remembers its cubic and its `t`, so a run of result points along one cubic becomes that cubic again, cut with de Casteljau), pixelart (#130: the palette a picture is painted in, how the layers of a frame come together, the sheet of all frames, a frame id read as a cut of it, and the revision) + pixelCel (the file of one layer on one frame, and its SHA-256 name worked out in the browser) + pixelDraw (what the tools do to the indices: brush, line, rectangle, ellipse, fill, resize about an anchor, merge, a palette colour taken out) + pixelEdit (every edit of layers, frames and cels as a function from one state to the next), polygon (an outline checked, cut into convex pieces and clipped to a rectangle), tile (the three kinds and their block shapes, reading a tile's pictures, the animation step), autotile (which quarter tile a neighbourhood asks for, the 47/16 tiles a block produces, 1 for a plain tile), tilemap (the cells of a layer, which neighbours a cell is joined to, the cells a stroke or a rectangle covers), tilemapCollision (the solid cells of a map, drawn in or out by their tile's collision scale and merged into rectangles, one group per one-way angle and per surface (`tileMaterials`, #193), the outlined cells as polygons — an autotile's outlines cut into its quarters and pieced together per cell like its picture —, where one sits in the world, and which cells of a placed map a world rectangle can reach, in cells (`cellsWithin`, #184)), tilemapMemo (what a map's cells work out to, kept while they hold the same: a version per cell map compared cell by cell, since strokes paint in place), animation (which frame at time t, a row's value there, what a row may aim at, the players on an object; KeyRef and its lookups) + animationPreview (the root at a frame, written into copies), curve (#247: a drawn easing evaluated at t — each segment a cubic solved for t by bisection, held to its rules by `sanitizedCurve`: ends at 0 and 1, no point overtaking another, no handle past its neighbour — and which keys and `Lerp` nodes use one) + curvePresets (the built-in eases traced as curves, a segment halved only where it strays), graphLookup (what a graph sees of the project: behaviors to run, state machines to read, curves to ease along), structTypes (#253: Struct assets as types — every typed struct variable, struct field and `Pack`/`Expand` found and brought in line with its asset, a renamed field carried through every value first, the cycle rule), Scope (how long a shared value lives: per instance, one for the run, or one per scene and rebuilt with every scene that loads, #164 — for a state machine's current state and for the values of a Variables asset), video (#402: a video or a clip of it by id, the stretch a clip Line truncated
|
||||
src/core/properties/ PropertySchema, kinds (one row per value kind), listItems (an item's kind, read off its value), resolve (the only override merge), deviation (what is gold), collider/camera/bounds geometry, clip (the region clip areas cut each object to, a convex polygon, #260) (with the side a one-way collider blocks from, and the rectangle around a shape a broad phase looks at first, #184), convex (capsules and polygons: rounded convex pieces against each other), ray (where a ray meets a shape, in double precision), rigidbody (what the engine reads off a rigidbody, a collider's material and the scene), joint (what it reads off a joint: its settings, where its far end is, its length and a hinge's limits, #154), aspect, enabled, editorRun, assetRef/textureRef (how a reference reads), declaredRows (the row a Property node declares in a shader or particle graph, and the params that node has in both)
|
||||
src/core/util/ list (take out, put back and move an item — what a command's undo is made of), vec (dot, the closest point on a segment — shared by the collider maths, the outline hit area and the polygon grips; a cubic Bezier at `t` and cut in two at `t` — shared by vector art, its Boolean operations and the curve grips), color (`mixColor`, channel by channel — keys of an animation, stops of a gradient, the particles' Mix colour), recorder (#253: assignments taken back last first — the undo of an edit whose reach is not known in advance)
|
||||
src/core/commands/ undoable commands (pixelArtCommands: one command runs any pixel art edit and keeps the state before it, #130), CommandStack, SetBagValueCommand base, RenameCommand base (undone by renaming back: variable, struct field, port), MoveCommand base (undone by moving back: tile picture, tilemap layer, vector shape), vectorArtCommands (#129: the shapes of a drawing, one gesture one command), one import command shape for texture/font/sound, require.ts (validated lookups, and what an instance may not change), naming.ts
|
||||
@@ -153,8 +153,9 @@ src/core/store/ serialize (untrusted JSON → Project), projectPlugins (w
|
||||
ReleaseStore/HttpReleaseStore (a project's released copy and the link to it, #266),
|
||||
AuthStore/HttpAuthStore (signing in and out, #177; changing the password, #182),
|
||||
FileStore (what the server holds of one kind: upload and list, no delete) + HttpFileStore (one
|
||||
generic client over the three file collections) — TextureStore, FontStore and SoundStore are its
|
||||
type parameters, nothing else
|
||||
generic client over the four file collections) — TextureStore, FontStore, SoundStore and
|
||||
VideoStore (#402: an upload with its status on the way to an MP4) are its type
|
||||
parameters, nothing else
|
||||
src/core/time.ts the global clock: one scaled step a frame, shared by the world, the particles, the shaders and the animation preview
|
||||
src/core/music/ time (ticks, 480 to the quarter, tempo and meter, a position as bar, beat and tick), sequence
|
||||
(the notes of a window of playing time, across the seam of a loop — a pure rule), Scheduler
|
||||
@@ -239,6 +240,10 @@ src/viewport/ WorldViewport (the drawing of a world or a holder, the ba
|
||||
resolution; registers vector art in both halves of the picture path; PaintCache keeps a
|
||||
painter's gradients and hands the ones it no longer draws to Pixi's texture garbage
|
||||
collector, and a texture fill waits for its picture instead of drawing without it, #186),
|
||||
VideoLibrary (#402: one `<video>` per key, its picture a Pixi texture made only once
|
||||
the video has shown its first frame (`openVideo`), a clip sent back to its start at
|
||||
its end, a run that starts every player over and hears it through the master, a
|
||||
player nobody asked for in 2 s let go) + the poster library of the video picker,
|
||||
PixelCels (the pixels of pixel art in memory: a painted cel is named at once, the ones the
|
||||
server lacks are uploaded before a save, #130) + pixelArtPicture (every frame composed on the
|
||||
CPU and laid side by side onto one canvas, so a frame is cut out like a texture frame),
|
||||
@@ -305,8 +310,11 @@ src/editor/ App → Editor (layout + ticker), PlaySession (the run),
|
||||
without changing the curve, the four point types and how a dragged handle moves the other,
|
||||
#129, #195), pointGrips + PointKindButton (a point with its handles as drawn, and the
|
||||
header's point type — shared by the Vector art and Curve tabs),
|
||||
fileImport (drop/paste) → assetImport (one upload pipeline, an ImportKind per sort) ← textureImport / fontImport / soundImport,
|
||||
FilePicker (the shared "+ → Texture / Font / Sound" dialog; ImagePicker/FontPicker/SoundPicker only describe their row),
|
||||
fileImport (drop/paste) → assetImport (one upload pipeline, an ImportKind per sort) ← textureImport / fontImport / soundImport / videoImport,
|
||||
FilePicker (the shared "+ → Texture / Font / Sound / Video" dialog; ImagePicker/FontPicker/SoundPicker/VideoPicker only describe their row;
|
||||
the video one shows a conversion on its way and asks again until it is done, #402),
|
||||
video/ (#402: VideoEditor + VideoCanvas, the Video tab — preview, ruler, clip strip and
|
||||
track on the time axis of the Sound tab's `waveView`; videoInfo, what a video is in words),
|
||||
framing (fit/marquee/snap maths), FormDialog (the small modal forms: label column, Cancel + confirm, one open at a time; `dialogButtons` is the closing row every dialog shares, the pickers' included), AssetTabEditor (the shared shell of the asset tabs), TabHeader (the bar across the top of a tab — asset tabs, the plugins and accounts lists, NodeBoard; drawn by `drawHeaderBar` like the toolbar and the breadcrumb),
|
||||
TabStrip + tabRow (the tab row and what it does when the tabs outgrow it; plugin tabs are inserted at run time),
|
||||
assetTabs (which asset kind opens into which tab, and so which kind has an editor at all),
|
||||
@@ -491,7 +499,10 @@ api/ PHP backend: index.php (routing and the sign-in gate; `ro
|
||||
lib/{Auth,UserStore,SessionStore}.php (#177: accounts in data/users.json, sessions in data/sessions/)
|
||||
lib/SnapshotStore.php (#231: data/snapshots/{projectId}/, one copy of the project per file)
|
||||
lib/ReleaseStore.php (#266: data/releases/{token}.json and which token a project has)
|
||||
data/projects/ persisted projects, data/textures/ + data/fonts/ + data/sounds/ content-addressed uploads,
|
||||
lib/{VideoStore,VideoQueue,VideoFormat,VideoWorker}.php (#402: a video named after its upload,
|
||||
the queue in data/videos/queue/, the one target format as plain rules, the conversion with
|
||||
ffmpeg; run by tools/video-worker.php in the container of tools/video/ and in CI)
|
||||
data/projects/ persisted projects, data/textures/ + data/fonts/ + data/sounds/ + data/videos/ content-addressed uploads,
|
||||
data/plugins/ editor plugins, filled from plugins/ by the build and the seeder; which of them run is written
|
||||
in each project (#119)
|
||||
tests/ Vitest unit tests tools/ seed-*, e2e (steps in tools/e2e/, rig in tools/e2e/lib/), smoke-*, lib/, assets/ (the walk demo's sheet)
|
||||
|
||||
+6
-3
@@ -19,7 +19,9 @@ php tests/php/accounts.test.php # the API's accoun
|
||||
php tests/php/snapshots.test.php # the snapshot store (#231), the same way
|
||||
php tests/php/releases.test.php # the release store (#266), the same way
|
||||
php tests/php/files.test.php # uploaded files in a folder per account (#344), the same way
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1976 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
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 # 1994 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
|
||||
@@ -328,8 +330,9 @@ seeders need (`PUT` does not create a project, `POST` hands out a random id). Ev
|
||||
server from `GAME_MAKER_BASE_URL`.
|
||||
|
||||
The jobs run in the image from `tools/ci/Dockerfile`: chromium with the Vulkan drivers and
|
||||
`xvfb`, `php-cli`, and the host's font packages, because the checks measure text widths to find
|
||||
click points. `ci.yml` asks for the label **`game-maker-wsl`** (#283): a runner in a WSL2 distro
|
||||
`xvfb`, `php-cli`, `ffmpeg` for the video worker (#402, `serve.sh` starts it beside the server),
|
||||
and the host's font packages, because the checks measure text widths to find click points. A
|
||||
change to the Dockerfile needs the image built again on both runners' hosts. `ci.yml` asks for the label **`game-maker-wsl`** (#283): a runner in a WSL2 distro
|
||||
on a Windows machine with an RTX 4070, so this host no longer carries the suite. The runner on
|
||||
this host is still registered with the label `game-maker-ci` and the same image; pointing both
|
||||
`runs-on` back at that label moves the runs here again.
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
A **video asset** is a moving picture a sprite wears through its **Texture** row, as it wears an
|
||||
image (#402). It has clips like a [sound](Sounds), stretches of it with a name, and a sprite can
|
||||
wear one of those instead of the whole video. Only Chrome is a target, and a video has no alpha.
|
||||
|
||||
## Getting a video in
|
||||
|
||||
**+ → Video** opens the picker: every video the server holds for you, each row with its poster,
|
||||
the name it was uploaded under and what it is ("1920x1080 · 12.5 s · 29.97 fps · with sound", the editor writes the x as a times sign).
|
||||
**Use video** makes the asset and opens its tab. **Upload…** is the way in for a new one, and
|
||||
**dropping a video file onto the canvas** does the same (`.mp4`, `.mov`, `.m4v`, `.webm`, `.mkv`,
|
||||
`.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
|
||||
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
|
||||
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.
|
||||
|
||||
**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
|
||||
disk, so the API holds none of it in memory: 300 MB went up with php-fpm at 14 MB.
|
||||
|
||||
## The asset
|
||||
|
||||
A video asset keeps the file and, copied in when it was picked, its **width, height, length,
|
||||
frame rate** and whether it **has sound**, so a sprite is measured before the file loads. Its rows:
|
||||
|
||||
| Row | |
|
||||
|---|---|
|
||||
| **Volume** | 0 to 1, how loud its sound is in a game |
|
||||
| **Loop** | on by default: at its end (or its clip's end) it starts over; off, it stops on its last frame |
|
||||
|
||||
**Create sprite from video** in its context menu makes a sprite of its size wearing it.
|
||||
|
||||
## The Video tab
|
||||
|
||||
The video on top, under it a ruler in seconds, the **clip strip** and a **track**:
|
||||
|
||||
- **A drag in the strip** (or in the track while no clip is picked) spans a new clip, one undo
|
||||
step, and picks it. **A click on a band** picks that clip, a click beside it in the track picks
|
||||
the video again.
|
||||
- The picked clip stands in the track between two handles; **dragging a handle** moves that edge.
|
||||
The picture follows the edge being dragged.
|
||||
- **A click in the ruler** shows that moment.
|
||||
- **Play** (or **Space**) plays the picked clip, else the whole video, with its sound and its
|
||||
Loop. The playhead runs along the ruler.
|
||||
- The wheel zooms the time, the middle button pans, **Fit** shows the whole video.
|
||||
|
||||
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.
|
||||
|
||||
## Wearing it
|
||||
|
||||
A sprite's **Texture** row offers the videos and their clips after the pictures ("Intro",
|
||||
"Intro / Clip 1"). Size, Color as a tint, a shader, colliders and the draw order work as with an
|
||||
image. Only the sprite's Texture row offers videos; a tile, a particle source and a vector fill
|
||||
take pictures only.
|
||||
|
||||
- **In the editor** a placed video plays in the scene view without sound, round and round, as a
|
||||
preview. The sprite's PREVIEW in the inspector plays it too, placed or not. A video nothing
|
||||
shows for 2 s is let go and starts over the next time it is shown.
|
||||
- **Play** starts every video over at its clip's start and lets it be heard through the master
|
||||
bus at the video's Volume. A pause holds it, Esc makes it a quiet preview again. A
|
||||
[released game](Concepts) does the same.
|
||||
- **Two sprites wearing the same clip share one player** and show the same picture at the same
|
||||
moment. A video plays in real time; the game's time scale does not slow it down.
|
||||
|
||||
Not yet: nodes to play, pause or jump in a video, or to hear when it ends. That is a follow-up.
|
||||
|
||||
## 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:
|
||||
|
||||
- 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
|
||||
a compose project of its own beside the web stack:
|
||||
`docker compose -f tools/video/compose.yml up -d`. The image is built like the CI image (see
|
||||
the head of `tools/video/Dockerfile`); a change to the worker's PHP needs a restart, not a
|
||||
rebuild. `docker logs game-maker-video` shows one line per video, with how long it took.
|
||||
- In CI, `tools/ci/serve.sh` starts the same worker beside the PHP server; the CI image has ffmpeg.
|
||||
|
||||
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.
|
||||
+1
@@ -12,6 +12,7 @@
|
||||
- [Vector art](Vector-Art)
|
||||
- [Pixel art](Pixel-Art)
|
||||
- [Fonts](Fonts)
|
||||
- [Videos](Videos)
|
||||
- [Sounds](Sounds)
|
||||
- [Instruments](Instruments)
|
||||
- [Patterns](Patterns)
|
||||
|
||||
Reference in new issue
Block a user