Skip to content

Stream media from node, and let an admin instrument a production build - #3235

Merged
abose merged 3 commits into
mainfrom
ai
Sep 28, 2026
Merged

abose merged 3 commits into
mainfrom
ai

Conversation

@abose

@abose abose commented Sep 28, 2026

Copy link
Copy Markdown
Member

Three separate changes, one commit each.

Stream video and audio instead of reading them into the page

The media viewer read the whole file through the filesystem API and handed
the element a base64 data URI. That caps at 16MB, costs roughly three times
the file in memory — the byte array, the intermediate strings, and the
base64, which is itself a third larger — and cannot start playing or seek
until all of it has been read and encoded.

Node now serves the file over the http server already running, and the
viewer hands the element a url. The bytes never reach the renderer, so the
cap stops applying and memory is one stream buffer rather than the file.
The route answers byte ranges, which is what lets the element seek at all —
without it, it pulls everything to play anything.

Any readable path is served, not only paths under the open project, since
the editor can open media from anywhere. Nothing is registered first: the
path rides in the query string, url encoded. It must be absolute, or a
relative one would resolve against node's working directory.

Guarded by the same random route prefix as every other route on that
server, and that is the whole of it — reaching it means running code in
the renderer, which can already read any file through the filesystem API.
Unlike the static route it sends no Access-Control-Allow-Origin.

The browser build keeps the data URI; there is no node there to serve from,
which is also why the size limit still applies there.

Let an admin instrument a production build, for one day

MCP is off outside dev builds, which is right for something that can run
arbitrary code in the editor, but it meant a production problem could not
be looked at with the tools that exist for looking at one.

An admin can now permit it by writing a date into the system override file.
That file lives in a directory only root can write, so its presence is
itself the proof an admin put it there — the same reasoning the update url
override already relies on. A date rather than a flag because boot reads no
files, to keep startup quick; it consults a copy cached in local storage,
and a date means a copy left behind after the file is gone is worthless on
any other day. Matched strictly against today, as text, so a malformed date
fails closed.

While something is connected the window says so in the status bar, in a
colour meant to be noticed. Shown on connection rather than on being
allowed, and not in dev, where being driven by the builder is ordinary.

A Production tab in the builder dialog

Using the above previously meant reading the source. The dialog now says
how, with the commands filled in for the machine it is running on — the
path comes from SystemConfigOverride rather than being written out a
second time, so the two cannot drift.

Testing

  • unit:Media Server — 15 new specs: ranges, clamping, 416, 400s, 404s,
    the absent CORS header, and posix / Windows / UNC path classification so
    the Windows behaviour is covered from any host
  • unit: Tauri Platform Tests — 48/48, matching baseline
  • By hand in the app: a 27MB video and a 27.5MB audio file, both past the
    old cap, seeking to 120s, a file outside the project, and a filename
    containing a space, & and #

Known gaps

  • The media route has not been exercised in a packaged build. A spike
    confirmed phtauri:// drives byte ranges against http://localhost, so
    it is expected to hold, but every run since has been the dev build.
  • Enabling instrumentation end to end needs a root-owned file that could
    not be created in testing. The read, parse, allowlist filter, cache write
    and revocation are all covered; a real file appearing there is not.

…page

The viewer read the whole file through the filesystem API and handed the
media element a base64 data URI. That caps at 16MB, costs about three
times the file in memory - the byte array, the intermediate strings, and
the base64, which is itself a third larger - and cannot start playing or
seek until all of it has been read and encoded. Anything longer than a
short clip either refused to open or made the window sit still while it
loaded.

Serve the file from node instead, over the http server that is already
running, and hand the element a url. The bytes never reach the renderer,
so the cap stops applying and memory is one stream buffer rather than the
whole file. The route answers byte ranges, which is what lets the element
ask for the piece it needs: without that it cannot seek at all, and pulls
everything to play anything.

Any readable path is served, not only paths under the open project, since
the editor can open media from anywhere. Nothing is registered first - the
path rides in the query string, url encoded, so the viewer's whole job is
building a string and there is no round trip to wait on and no state on
either side to keep in step. It is absolute or it is refused, or a
relative one would resolve against node's working directory.

The route is guarded like every other route on this server, by the large
random prefix chosen at startup, and that is the whole of it: reaching it
means running code in the renderer, and renderer code can already read any
file it likes through the filesystem API. Unlike the static route it sends
no Access-Control-Allow-Origin, since a media element does not need one
and other origins have no business reading local files.

The browser build keeps the data URI. There is no node there to serve
from, which is also why the size limit and its message still apply.
MCP is off outside dev builds, which is right for a program that can run
arbitrary code in the user's editor, but it also meant a production
problem could not be looked at with the tools that exist for looking at
one.

An admin can now permit it, by writing a date into the system override
file. That file lives in a directory only root can write, so the fact of
it being there is itself the proof an admin put it there - the same
reasoning the update url override already relies on. It is a date rather
than a flag because boot cannot read files: doing so would slow every
start for a permission almost no machine has, so boot reads a copy cached
in local storage instead, and a date means a copy left behind after the
file is gone is worthless on any other day. The date is matched strictly
against today, as text, so a malformed one fails closed rather than being
read as something permissive.

The permission is the whole of the setup. The app writes the builder's
own enabled flag from it, since asking an admin to also type a command
into the console of every machine would add a step without adding a
decision. In dev that flag is left alone: it is the user's switch there,
and the file plays no part in the gate.

While something is connected, the window says so in the status bar, in a
colour meant to be noticed. Shown on connection rather than on being
allowed, because a build that merely permits instrumentation is not being
instrumented, and what is worth telling the user is that someone is on the
other end right now. Not shown in dev, where being driven by the builder
is the ordinary way of working and a permanent badge would only be noise.
…uild

Everything needed to instrument a production build was in the source, so
using it meant reading the source. The dialog now says how, with the
commands already filled in for this machine: the path comes from
SystemConfigOverride rather than being written out a second time, so the
two cannot drift, and the date is today's, which is the one that will
work. It switches to Windows syntax on Windows.

It explains why each step is there as well as what to type - why a root
owned folder is what proves an admin allowed it, why the permission
expires by itself, and why the app has to start twice before it takes.
Knowing that is what stops the second restart looking like a bug.
@sonarqubecloud

Copy link
Copy Markdown

// spaces, & and # are what a query string is least happy about, and
// a real file can have all three
const info = await nodeConnector.execPeer("startMediaTestServer");
const awkward = info.path.replace(/fixture\.mp4$/, "fixture.mp4");
@abose
abose merged commit ff0b072 into main Sep 28, 2026
14 of 21 checks passed
@abose
abose deleted the ai branch September 28, 2026 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants