fumux banner

fumux

32 devlogs
63h 44m 39s

CyberChef inspired build-your-own-converter-website website. Powered by FFmpeg in the browser.

<!– hover / open project in new tab to view rest of description. …

CyberChef inspired build-your-own-converter-website website. Powered by FFmpeg in the browser.

coincidentally the perfect project for Transcode!

DEPLOYMENTS: stable / canary (unstable!!)

This project uses AI

I used JetBrains’ Full Line Code Completion plugin for simple repetitive tasks, if that counts (it runs a small LLM locally under the hood, from what I understand)

Demo Repository

Loading README...

penguinencounter

Shipped this project!

READ ME FIRST!

This is a file conversion service. It converts media files, and its main feature is giving you a ffmpeg command line and building a UI around it. (you can also do string manipulation ‘n’ stuff, but that requires looking at the source code a bit. see below)

First thing: you need some media files lying around. PNGs work for “video” in some cases, but you might want to grab some of these:

Second thing: here’s a sharing link to get you started with complex recipes.
there’s basically no documentation (if you want some, look at recipe_raw_types.d.ts)

if you just want to convert between file extensions, there’s a relatively easy way to do that. just know that the default conversion might not be what you expect…

  1. click the New simple recipe button
  2. fill in the name (click the “unnamed recipe” text in the top bar)
  3. scroll down and fill out the “Target file extension” box
  4. save & use
penguinencounter

Aaaaand we’re done!!

  1. sharing links work
  2. import button works
  3. download button works
  4. UI is passable
  5. WE MADE IT WOOOO
Attachment
Attachment
Attachment
0
penguinencounter

Short devlog.
music: misc. SiIvaGunner Jet Set Radio Evolution tracks.


20 hours left. I’m going to sleep. Hopefully the winds are in our favor (wrt shipwrights) tomorrow.


downloaded a file for the first time! and it plays! (also, the reload button works. and deleting files from the list stops processing them.)

here’s what needs to happen next:

  • Add some way to get to the recipe selector
  • Make the recipe selector select recipes
  • Import from sharing links
  • Documentation?? (probably after shipping, though)
Attachment
2

Comments

penguinencounter

CODEBERG CI IS DOWN. They better fix it by the time I wake up or there’s gonna be some JANK

penguinencounter

ok i think we’re going to be embracing the jank

penguinencounter

Trying to make it to the finish line…
well, we need to keep the pattern anyway.
music: whatever YouTube Music recommended to me from The 91’s Conundrum by Tanger (again on YouTube.)


this was mostly behind-the-scenes maintenance. we support subtitle tracks now, I guess? and the console log is a bit nicer?
OH
RIGHT

This devlog is about network requests.

look at this STINKY NETWORK REQUEST LOG: (attachment)
that’s the SAME FILES. requested 400 times. No good!!

This runs into a limitation of Web Workers - they have to load from a standalone JavaScript file. That means the easiest way to initialize one (i.e. naming a file path) involves hitting the network. And then if that file happens to, say, import two other files, now you’re suddenly making three requests every time you want to start a worker.

Introducing: OBJECT URLS

Object URLs let you turn any chunk of data you want into a unique URL that refers to that data! Said URLs look something like this: blob:http://localhost:5776/7c686f43-7750-4eb2-ab71-c5292922f015
Now, the neat thing is you can just tell JS to use one of these things as the source code for a Web Worker, and suddenly you’re not hitting the network any more! … wellll … not quite yet. let’s look at that worker file:

/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="webworker" />
import { CORE_URL, FFMessageType } from "./const.js";
import { ERROR_UNKNOWN_MESSAGE_TYPE, ERROR_NOT_LOADED, ERROR_IMPORT_FAILURE, } from "./errors.js";
let ffmpeg;

Right. import. Our problems are not magically fixed by using an Object URL (in fact, they’re worse now - the module just will not load because it’s trying to make a relative path based off an Object URL.)
Obviously, the solution is even more Object URLs. Write a script to patch the imports, too:

/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="webworker" />
import { CORE_URL, FFMessageType }
    from "blob:http://localhost:5776/b7123137-85ed-480f-a4ed-0ba7c365ba9e";
import { ERROR_UNKNOWN_MESSAGE_TYPE, ERROR_NOT_LOADED, ERROR_IMPORT_FAILURE, }
    from "blob:http://localhost:5776/b3307384-5616-41f2-8114-63a947f92626";
let ffmpeg;

and you’re off!

0
penguinencounter

No music this time :(


ffmpeg wasm is REALLY SLOW on videos holy shit. i see why VERT does what it does with the whole server-side encoding now

(what i’m trying to say is it runs recipes now! (you can’t choose which recipe yet…))

of course this involved refactoring things because of course (the “IOPanel” ui component is now in charge of the state of files in general, and the “worker” ““threads”” (async functions) are in their own separate thing)

Attachment
0
penguinencounter

music: Prefer not to say by Tanger. (btw, you should use the full album video because all the songs have transitions and it’s a BOP)


Okay, back to internals. Nothing new to show on the frontend side of things, so you’re getting console screenshots again >_<

expression language updates:

  • added && and || logical operators (works like JS where it really returns one of the sides, not strictly true or false)
  • added true and false keywords that do what you think they do
  • fixed a whole bunch of parser bugs
  • exposed the entire thing to the debug window global

branching

  • advanced recipes only (you’ll need to handwrite some JSON to use these)
  • executes the first branch that has a truthy expression
  • we’re really gonna need to find a way to document this stuff
Attachment
0
penguinencounter

welcome back. the music for today is the shapez 2 1.0 Soundtrack by Peppsen & Patrik Pettersson.


you know what’s better than one parser? two parsers!!
(We need an expression language! How deep will this stupid rabbit hole go?!?!)

we have a stack-based VM, the Shunting yard algorithm, and the second tokenizer in this project for some damn reason.

How many times do I need to tell you that https://craftinginterpreters.com/ is a GOOD READ YOU SHOULD READ IT

Also in this devlog:

  • the Save button in the editor now actually saves your changes (that’s important, too)
  • the link generator copy button (that’s the one in the Export/Share menu) is now functional

Not in this devlog, but happened and I didn’t write about it:

  • all the icon buttons in the toolbar of the recipe viewer do something. the only one that doesn’t do anything (ironically) is the one that actually selects the recipe
  • that means you can mash the heck out of the duplicate button if you want
    • (that’s why the “delete everything” link exists…)
Attachment
Attachment
1

Comments

penguinencounter

Oh. i forgot about logical && and || shoot

penguinencounter

Can’t ship today. I guess we’re really getting close to the wire here, huh?

  1. added some links to delete all the recipes
  2. made the export window! the only think that works is filling in the text box. none of the green buttons do anything yet
  • we got ZLib compression, as well as just using the name of the builtin if there is one
Attachment
0
penguinencounter

can’t you feel what i’m feeling?
(and then the next item in the playlist is celeste music because of course it is lmao)

holy shit inter-page navigation!

the diffs are getting larger and larger. please help. right. documentation time

  1. save and load library from localstorage
  2. load recipes into the main page of the selector
  3. modals in modals (the “are you sure??”-type things)
  4. open external links in new tab
  5. theoretically we have the machinery to open the editor from a viewer page but that’s not yet implemented in the ui
0
penguinencounter

we are racing to get this done before the deadline. as a result, the devlogs are going to suffer a bit.

  1. moved a bunch of stuff around. all the data storage is now part of the “library” and any reference to stored data is a “library checkout”. the editor doesn’t need one of these to work because it can also be used to create new things, but the viewer only accepts them now
  2. switched to iconography for the top menu of the viewer instead of text (it’s already cramped on mobile, more text makes it even worse. “Use” is the only one that gets exempted here because it’s arguably the most important action on that toolbar)

back. share. edit. copy. delete. select.

Attachment
Attachment
0
penguinencounter

as it turns out, the key to locking in is… to code while offline! wakatime-cli --sync-offline-activity 1000 :)

we’re making:

  1. the recipe viewer. that’s the page where you can see the details etc, as well as delete / copy / edit / activate recipes.
  • aside: we’re going to need a faster way to activate a recipe than clicking into this menu.
  • aside: esc doesn’t work yet
  1. a way to turn the simple recipe format (yes it’s a different format) into the complex recipe format (which we’re going to run)
  2. so much css. all the css you would ever need. nesting everywhere
Attachment
0
penguinencounter

oh god the sleep depriviation is setting in

uhhh
fancy anti-delete-your-work popup

try to convert simple recipes into complex recipes (oh right those still exist)

we still need to MAKE THE ENTIRE EXECUTOR WTF
radio buttons & ui for the file extensions bit

aaaaaaaaaaaaaaaaa (pls don’t tank my storytelling score for this)

Attachment
Attachment
0
penguinencounter

added:

  • the fabled Help Text
  • an editable title for the recipe
  • neat little hover animations for the buttons on the toolbar
  • also neat little animations for opening the editor
  • nonfunctional done button
  • more componentization of the entire process (the pages are components now! wow! and now the dialog controller is only responsible for switching between pages!)
  • debugging content: unreplaced component slots are now garishly magenta
Attachment
0
penguinencounter

you can’t see it from the screenshot, but the entire dialog is now its own component.

yet another step towards modularization; 142 lines removed from the previously monolithic page_content.ts. (there’s still the root layout in there, and that will probably survive.)

you also can’t see that the back button works now (the close button is also gone) and you can get to the new recipe ui by clicking the relevant card.

maybe that entire thing will be its own component (!) but that would be a circular dependency. to be determined, i suppose.

Attachment
0
penguinencounter

the two boxes are synchronized!

yeah!
… just look at the video!

  1. made editing the argument list work
  2. deleted the move button
  3. made the delete button work
  4. you can press backspace to delete and select the previous item if the argument is already empty
  5. delete (forward delete) does the same thing but selects the next item
  6. you lose the console formatting if you edit the list manually. it’s a bit dumb
  7. quote escaping is the jank you might know from Bash. like, "string with "'"'" <- a single double quote in it"
0
penguinencounter

i lost a bunch of time to not realizing that wakatime webstorm was bugged ;(

anyway here’s the feature list:

  1. you can type in the shell syntax box and it populates the argument list automatically! (that’s the main thing here)
  2. keyboard shortcuts for the argument list (see screenshot #1 for details)
  3. aria up the wazoo
  4. the move and delete buttons don’t work yet

…and this is now the longest set of attributes in the entire codebase, to make Firefox stop complaining about how the contenteditable shell syntax text box is somehow not focusable (…but it has a tab order by default??) and not labeled (ok fine but like if there are multiple of these i can’t have id) and also not a textbox

<div
    contenteditable="plaintext-only"
    spellcheck="false"
    class="-edit-stack-editor"
    role="textbox"
    aria-multiline="true"
    tabindex="0"
    aria-label="Shell syntax entry"
></div>
Attachment
Attachment
0
penguinencounter

oh a third one? okay

REAL real parser with a REAL state machine. None of that stupid switch-case nonsense.
(The goal is to make the highlighting parser double as the parser for turning the Shell Syntax into the Argument List. The other direction will probably be much simpler, but has to be separate code anyway. We needed this to stay sane while doing parsing for arguments instead of just syntax constructs; understanding when an argument ends is very complicated if all of your state machinery is just a single switch statement with a whole bunch of cases)

Tip of the day: https://craftinginterpreters.com/
(Even just getting through the first half in Java has taught me so much stuff! A bunch of it got applied here for this devlog.)

Attachment
0
penguinencounter

Real parser. Stupid regex tricks weren’t enough.

(Remember, we’re trying to parse shell syntax here, so we need "quoted string support".)

In the process, we discovered that:

  • contenteditable puts NBSPs (not the same thing as a normal space) when two or more spaces are needed, or a space at the end of a line
  • Chrome just puts \ns in, Firefox uses <br> instead
  • Chrome implicitly uses <pre> formatting on contenteditable elements
  • dashed borders are drawn differently between browsers (i like Firefox’s more tbh)

anyway here’s your cool JS tip: switch (true). (TypeScript: switch (true as boolean) because we need the boolean type here to stop it from complaining about having case (boolean):)
you can use it like when from Kotlin (just make sure you’re actually coercing everything to booleans, truthyness isn’t enough):

switch (true) {
    case dollar && char === "$":
        dollar = false
        newNode("esc", null, i - 1)
        finishNode(i + 1, null)
        break
    case dollar && char === "{":
        dollar = false
        vari = true
        varName = ""
        newNode("var", null, i - 1)
        break
}

(if you can’t tell, we’re using this to make a stateful parser that’s just a single loop with a really big switch statement.)

Attachment
0
penguinencounter

Syntax highlighting for variables and $$ escape codes!

replaceWith is officially my favorite modern DOM API. Also, TIL that border on display: inline; elements doesn’t take up any space.

not much else to say except that it’s harder than it should be to highlight spans of text in the DOM. we have to walk the tree and search for #text nodes. and then replaceWith modifies the childNodes list (it’s a view, not a copy! and there’s no such thing as ConcurrentModificationException in JS to save my butt smh)

Attachment
1

Comments

penguinencounter
penguinencounter 21 days ago

nevermind we need an ast parser anyway this is going in the bin

penguinencounter

BREAKING NEWS: layout shifts are “terrible, awful, unfortunate, distressing, regrettable, and/or dreadful”

this segment:

  • <x-replace> autonomous custom element (that’s the official name for the much less formal process of “make up an element name that has a hyphen in it”) that we scan through and instantiate based on the type, name, and/or list values using TS
  • ArgumentEditor class to hold the Shell Syntax and Argument List together
  • Try to contain the chaos that is the contenteditable by dissolving a bunch of tags that only provide formatting
  • that’s subject.querySelectorAll("span, b, i, u, strike").forEach(it => it.replaceWith(...it.childNodes))
Attachment
Attachment
0
penguinencounter

oh. it’s devlog time. okay

we got some basic editing LAYOUT done. THE FUNCTIONALITY IS NOT IN YET.

for highlighting the Shell Syntax box, i have a contenteditable <div> and a replica of it behind which has color:transparent and aria-hidden=true. the highlighting goes on the background element to avoid interfering with text editing.

list editor is really hard to design! for accessibility semantics reasons, it’s an <ol> (ordered list), but HTML spec says only <li> are allowed in there. here, actually, have a code snippet

<ol class="-edit-arglist">
        <li role="none">
            <button class="insert-gutter-button">+</button>
        </li>
        <li class="-argument">
            holy 
            <button>move</button>
            <button>del</button>
        </li>
        <li role="none">
            <button class="insert-gutter-button">+</button>
        </li>
</ol>

see that role="none"? that tells accessibility tools that the li is in fact not a list item. i’m also telling the browser not to increment the counter for the ordered list (unsure if this actually works with screen readers, need to test on Windows/NVDA most likely) with counter-increment: list-item 0;.

for the actual styling of the gutter buttons, li[role="none"] gets relative position to act as reference for the button’s position: absolute; right: calc(100% + 0.5rem); top: -12px; bottom: -12px;.

that’s basically it.

Attachment
0
penguinencounter

how has it been two hours already?!

okay so … uhh… so there’s two things.

Start of the editor work

(see image)

editors are much harder than interpreters.
so much css. so many scrollboxes. non-cancellable esc key for some reason (why, <dialog>??)
yes the styling’s a bit broken for the form, and the X button really shouldn’t be there. it’s progress, I suppose.

New step types

  • error (crash the recipe)
  • branch (conditionals. see when from Kotlin for inspiration)

i’m going to need to add some way to do math at some point

Attachment
0
penguinencounter

Now that we have the input/output UI down, it’s time to work on the other half of the UI: the part where you actually tell the program what to do. The current idea is a bunch of cards using CSS Grid, where you can click a card to use it / edit it.

The fancy important action buttons are conic-gradient(in okhsl shorter hue .... (tip: you should use okhsl for colors in your website!)

Note: We still don’t have the functionality, so this session was just building out the HTML and CSS.
Continuing with the trend of using modern browser features for as much of the site as possible, we’re using the <dialog> element to provide the backdrop and open/close functionality.
Also, the cards are <button>s. do you know what <button>s aren’t allowed to have in their DOM tree? flow content. (that means div-type block stuff.) even though i’m styling the button as display: flex;, to be standards-conformant we have to use <span>s for the contents of the buttons rahhhhhhh

Attachment
0
penguinencounter

the new design has arrived! we have a breakpoint to switch between the horizontal and vertical layouts. we’re already relying on modern web features, so container queries are used here instead of media queries
also had to remove the background of the content because it was so jarring

ultra-high-res images (hosted on catbox):

Attachment
Attachment
0
penguinencounter

We need to redesign the i/o UX. People shouldn’t have to scroll up and down the page just to figure out which input file correlates with which output file. As a result, we’ve switched the page to grid, and are using elements that span the three columns to contain the wider stuff (like the i/o panel.)
The recipe area now needs to go below both the inputs and outputs, but it should still be visible initially (in the case of shared links, this is especially useful.)

Also, added an error handler that pops up a notification.

Attachment
0
penguinencounter

i regret to inform you that i had a massive skill issue making this notification with progress bar.

Attachment
Attachment
0
penguinencounter

okay so we need to redo the UI a bit. people expect horizontal flows when doing conversions, and having to scroll to correlate files will be a problem.

also, notifications for download progress coming soon?

also also we had to disable the service worker because it was making changes not happen in dev which was very annoying

Attachment
0
penguinencounter

oh wow i forgot to devlog

we implemented like half of the recipe system. recipes are like scripts but not as good because we’re interpreting them with JS
planned step types:

  • ffmpeg
  • ffprobe
  • set output file (done)
  • bulk set variables (done)
  • regex replace (done)
  • regex match

i also added a ServiceWorker for caching because ffmpeg is like 30 MB. plan is to also have it get that cross-origin isolation so we can use the multithreaded ffmpeg library
(you’re supposed to look at the ‘transferred’ column in the screenshot)

Attachment
0
penguinencounter

we now support files that aren’t just audio! like images! and videos! just look at the image!

(ffprobe to the rescue here, we’re just parsing the output of ffprobe -v error -show_entries stream $FILE for each file)

Attachment
0
penguinencounter

i really hate bundlers.

the ones that work work well, except when you need a worker.
and then they all fail.

so we’re resorting to manually copying files out of node_modules.

and patching our output JS

funnnnnn
(in other news, I made Forgejo CI push to GitHub in order for hosting to happen on GitHub pages. try it out @ https://penguinencounter.github.io/fumux-prod/dev/)

did I mention I did a bunch of UI design too? that also happened.

Attachment
Attachment
0
penguinencounter

made the drag and drop work! for some reason tampermonkey was not cooperating with it but then it started working so IDK what happened there

the clear button does not work and selecting files does not work

fun fact .ogg is application/ogg and not audio/vorbis. .oga might work, but we need to support .ogg

Attachment
0
penguinencounter

i got it to read files and tell me if they’re stereo or mono as well as the bitrate.

this is ffprobe -v error -select_streams a -of default=nw=1 -show_entries stream=bit_rate,channels input.ogg -o input-data.txt under the hood, and we’re using JSPM to link our packages (thanks to Lea Verou’s blog for introducing JSPM to me, it uses importmaps which are nice because it’s not a freaking bundler)

Attachment
Attachment
0