Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
151357 stories
·
33 followers

From 10 Failed Stacks to Production: How a Data Scientist Built a Job Board with Wasp, a Full-stack Framework for the Agentic Era

1 Share
note

Hireveld is currently down while Marcel works on a major refactor - but it's real, we swear! It'll be back up soon.

Marcel Coetzee is a data scientist and AI consultant based in South Africa. With a background in actuarial science and data science, he runs his own consultancy. He also builds SaaS products on the side. His latest project, Hireveld, is a job board tackling South Africa's broken hiring market. He built it entirely with Wasp after trying nearly every other stack out there.

Tell us about yourself. How did you end up building web apps as a Python developer?

My path has been a bit unconventional. I started in actuarial science, which involves insurance, mathematical statistics and risk modeling. From there I moved into data science, then data engineering, and eventually into building products. Python has been my main language through all of that.

Today I run my own consultancy doing data engineering and AI work. But I've always wanted to build my own things too, so I started learning the JavaScript ecosystem and working on SaaS products on the side. I'm not a JS native by any means, but with the rise of agentic coding tools, I realized I could finally turn my ideas into real full-stack applications without spending years mastering every corner of Node and React.

What's Hireveld, and what problem are you solving?

Hireveld homepage showing 'Hire without the markup' headline
Hireveld's landing page - hire without the markup

Hiring in South Africa is expensive and opaque. Recruitment agencies take a massive cut of annual salary. The established job boards charge thousands of rands just to post a single listing. And too many roles still get filled through personal connections rather than merit.

I built Hireveld to change that. Employers post for free, applicants get ranked anonymously, and employers pay a flat fee to reveal candidates. It's simple, it's cheap, and it puts merit first. The whole thing runs on Wasp - auth, background jobs for expiring old listings, transactional email, payment integration, the works.

You mentioned trying about 10 different stacks before landing on Wasp. What happened?

Yeah, I went through quite the journey. I started with PocketBase because I liked the idea of owning my code and not being locked into a cloud platform. It's a solid tool, but I quickly realized I needed PostgreSQL for search, background jobs, and a frontend that wasn't stitched together by hand. It just didn't scale to what I was building.

Then I tried Next.js, Nuxt, Svelte - they're decent, but those codebases can grow extremely quickly. As someone who's still relatively new to the JS ecosystem, I'd hit the limits of my knowledge fast. I even tried Django, thinking I'd stick with Python, but it's accumulated so much over the years. Too much magic, too much stuff.

My philosophy is: the projects that succeed expose as few abstractions as possible to the user. I try to keep myself at the highest level of abstraction I can. When I found Wasp on GitHub, the config file clicked for me immediately. You declare what you want - auth, database, jobs, email - and it all works together. I was writing actual product code on day one instead of gluing infrastructure together.

Don't prioritize the important over the urgent. With other stacks I was spending time on infrastructure decisions that felt important but weren't getting me closer to a product. Wasp let me focus on the urgent thing: shipping.

You're a big advocate for agentic coding. How does Wasp fit into that workflow?

This is where Wasp really shines, and honestly I think more people need to know about it. I've been building Hireveld almost entirely through agentic coding - Claude Code in the terminal - and after trying 10 different things, Wasp is by far the best experience for AI-assisted development.

Here's why: context is the precious commodity. Every line of code in your project takes a chunk of the model's context window. Wasp keeps the codebase tight and small.

The .wasp config file means the AI can understand your entire app's architecture at a glance - your routes, your auth setup, your jobs, your entities. Instead of the agent crawling through hundreds of files trying to figure out how things connect, it's all declared in one place. And because Wasp is opinionated and constrained, the agent doesn't try to do 50 different things. When something is wrong, the compiler screams. That tight feedback loop is exactly what agentic coding needs.

Wasp respects the model's context length. It keeps things tidy. The constraint is the feature - it's what keeps both you and the AI from spiraling into a 20,000-line mess.

I should say - I'm not blindly vibe coding. I know where my files are, I know my routes, I hand-edit the main.wasp file when I need to. I take testing seriously, both e2e and unit tests. QA is the layer where you, the human, decide what you actually want to build. But Wasp gives me the structure to stay at a high level and be productive, even as someone whose main language is Python. Also bring my data science background to bear by simulating data to gauge how the system would react to real traffic.

You also contributed back to Wasp - tell us about the Microsoft Auth integration.

Hireveld job search showing filters and a Junior Web Developer listing
Hireveld's job search interface

Hireveld targets the South African enterprise market, and enterprises run on Microsoft. They need Entra ID (Azure AD) for single sign-on - it's non-negotiable. When I started building, Wasp didn't have a Microsoft OAuth provider. With most frameworks, that would mean either paying a fortune for a third-party service or building a fragile custom integration that becomes tech debt.

But Wasp's codebase is approachable enough that I could build the provider myself and contribute it back. The PR process was great - Carlos and the team were welcoming and helpful. That's the sweet spot I was looking for: a framework that's batteries-included enough that I'm not rebuilding auth from scratch, but open enough that when I need something custom, I can add it without fighting the framework.

The community in general has been one of the best parts. The developers are genuinely friendly, my contributions felt valued, and I can tell the team takes agentic coding seriously - they maintain a Claude Code skill, they keep their prompts updated, they engage with the tooling ecosystem. That's an unusual level of involvement for a framework team.

What would you say to a developer considering Wasp for their next project?

If you're building a full-stack web app in 2026 and you're using AI tools to code - which you should be - try Wasp. Seriously. I went through PocketBase, Next.js, Nuxt, Svelte, Django, and more. Wasp is the only one where I felt like I was building my product from day one instead of fighting my tools.

It gave me auth, type-safe full-stack operations, background jobs, and transactional email - all wired together from a single config file. Everything else - the ranking algorithm, payments, file storage - I built on top of what Wasp provided. That separation is what made it possible to ship as a solo developer.

And if you're coming from Python or another ecosystem and you're intimidated by the JavaScript world - don't be. Wasp abstracts away enough of the complexity that you can stay at a high level and be productive. I'm proof of that.

Wasp is the full-stack framework for the agentic era. It's the one that lets you focus on what you're building, not how you're building it.


Marcel Coetzee is a data scientist, AI consultant, and SaaS builder based in South Africa. You can find him on GitHub and reach out to him on coetzee.marcel2@gmail.com

Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

The WebCodecs Handbook: Native Video Processing in the Browser

1 Share

If you've ever tried to process video in the browser, like for a video editing or streaming app, your options were either to process video on a server (expensive) or to use ffmpeg.js (clunky). With the WebCodecs API, there's now a better way to do this.

WebCodecs is a relatively new API that allows browser applications to process video efficiently with very low-level control.

In the past, if you wanted to build, say, a video-editing app or live-streaming studio or anything that required 'heavy lifting', you needed to build a native desktop application. Many SaaS tools like Canva got around this with server-side video processing, which provided a much better UX, but which is much more complex and expensive.

With WebCodecs, it's now possible to build these apps entirely in the browser, without requiring users to download and install software, and without expensive, complex server infrastructure.

This isn't theoretical. Video Editing tools like Capcut saw an 83% boost in traffic after switching to WebCodecs + WebAssembly [1]. Utility apps like Remotion Convert and Free AI Video Upscaler (both open source) process thousands of videos a day with zero server costs and no installation required [2].

Remotion Convert

WebCodecs is even being used for entirely new use cases, like generating videos programatically [3].

If you're building any kind of video app, it's worthwhile to at least know about WebCodecs as an option for working with video in the browser.

In this guide, we will:

  1. Review the basics of Video Processing

  2. Introduce the WebCodecs API

  3. Discuss Muxing + Demuxing to read and write video files

  4. Build our own video conversion utility to convert videos between webm + mp4, and apply basic transformations

  5. Cover some production-level concerns

  6. Discuss additional resources

The goal of this article is to be a practical entry point and introduction to the WebCodecs API for frontend developers. It'll teach you how the API works and what you can do with it. I'll assume you know the basics of Javascript but you don't need to be a senior developer or a video engineer to follow along.

At the end, I'll mention additional learning resources and references. In future tutorials, I'll go more in-depth on specific topics like building a video editor, or doing live-streaming with WebCodecs. But this handbook should provide a solid starting point for what WebCodecs is, what it can do, and how to build a basic application with it.

Table of Contents

Prerequisites

You don't need to be a video engineer to follow along, but you should be comfortable with:

  • Core JavaScript, including async/await and callbacks

  • Basic browser APIs like fetch and the DOM

  • What a File object is and how file inputs work in HTML

  • A general sense of what HTML5 is (we'll use it briefly, but won't go deep)

No prior knowledge of video processing, codecs, or media APIs is required — that's what the first half of this handbook covers.

Primer on Video Processing

Hold your bunnies, because before getting into WebCodecs, I want to make sure you're aware of what codecs are before we even consider putting codecs on the web.

Video Frames

I presume you know what a video is. Ironically the 'video' below is actually a gif, but you get the idea.

Big Buck Bunny, an opensource video

Videos are just a series of images, shown one after the other, in quick succession. Each image is called a Video Frame, and each frame is associated with a timestamp. When a video player plays back the video, it displays each video frame at the time indicated by the timestamp.

Video Frames

Every frame in the video is made of pixels, with a 4K video frame containing approximately 8 million pixels (3840*2160 = 8294400).

VideoFrames have pixels

Each pixel itself is actually made of 3 components: a Red, Green, and Blue value (also called RGB value).

RGB Channels

Each of of the R, G and B color values is stored as an 8-bit integer, ranging from 0 to 255, with the number indicating the intensity of the red, green, or blue color component.

uint8 color channel

Combining the intensity of each of the R, G, and B components lets you represent any arbitrary color on the color spectrum:

RGB Color value examples

So for each pixel, we need 3 bytes of data: 1 byte for each of the R, G, and B color values (1 byte = 8 bits). A 4K video frame therefore would contain ~25 Megabytes of data.

RGB Channnels

At 30 frames per second (a typical frame rate), a 1 hour, 4K video would be around 746 Gigabytes of data. If you've ever downloaded a large video or recorded HD video with your phone camera, you'll know that video files can be large, but they're never that large.

In reality, actual video files you might watch on YouTube, record on your phone camera, or download from the internet are ~100x smaller than that. The reason actual video files are much smaller is because of video compression, a family of very sophisticated algorithms that help reduce the data by ~100x.

Without this video compression, you wouldn't be able to record more than 10 minutes of video on the latest high-end smartphones, and you wouldn't be able to stream anything HD on a high-end home internet connection.

As sophisticated as our modern devices and internet connections are, without aggressive video compression, we wouldn't be able to watch, record, or stream anything in HD.

Codecs

A codec is a fancy word for a video compression algorithm. There are a few established codecs / compression algorithms, such as:

  • h264: The most common codec. If you see an mp4 file, it most likely uses the h264 codec.

  • vp9: An open source codec used commonly by YouTube and in video conferencing, often found in webm files.

  • av1: A new open source codec, increasingly being used by platforms like YouTube and Netflix.

How these algorithms work is too complex and out of scope for this handbook. But at a very high level, here are some major ways these algorithms compress video:

Removing detail

All these algorithms use a technique called the Discrete Cosine Transform to "remove details". As you remove "detail" from the video frame, the frame starts looking "blockier". This technique is so effective, though, that you can compress a video frame by ~10x before the differences start becoming visible to the human eye.

For the curious, you can see this video by Computerphile on how the DCT algorithm works.

DCT algorithm removing details

Encoding frame differences

When you actually look at a sequence of video frames, you'll notice that visually they're quite similar, with only small portions of the video changing, depending on how much movement there is.

These codecs/compression algorithms use sophisticated math and computer vision techniques to encode just the differences between frames,.

Frame Differences

You therefore only need to send the first frame (a Key Frame) – then for subsequent frames you can send the "frame differences", also called Delta Frames, to reconstruct the each full frame.

Key Frames vs Delta frames

In practice, for an hour long video, we don't just encode the first frame and store millions of delta frames. Instead, algorithms encode every 60th frame or so as a Key Frame, and then the next 59 frames are delta frames.

This technique is also highly effective, reducing data used by another ~10x. The distinction between Key Frames and Delta Frames is one of the few bits of "how these algorithms work" that you actually need to be aware of.

There's a number of other details and compression techniques that go into these compression algorithms that are out of scope for an intro article.

Encoding & Decoding

For video compression to work, we need to be able to both compress video (turn raw video into compressed binary data) and then decompress video (turn the compressed binary data back into raw video frames).

Turning raw video frames into compressed binary data is called encoding, and turning compressed binary data back into raw video frames is called decoding. The word codec is just an abbreviation for "encode decode".

VideoEncoder and VideoDecoder

From a practical, developer perspective, you don't need to know how these codecs work, but you do need to know that:

  1. There are different video codecs, like h264, vp9, and av1

  2. When you encode a video with a codec (like h264), you need a video player that can support the same codec to play back the video.

  3. Encoding video takes a lot more computation than decoding video, so playing 4K video on a low-end phone is fine, but encoding 4K video on it would be super slow.

  4. Most consumer devices (phones, laptops) have specialized chips designed specifically for encoding and decoding video, making encoding/decoding much faster than if run on the CPU like a normal software program. This is called hardware acceleration.

In practice, there are only a handful of video codecs, because the entire world needs to agree on standards, so that video recorded on an iPhone can be played back on a windows device.

Containers

Most people haven't heard of h264 or vp9. When you think of video files, you typically think of file formats like MP4 or MKV. These are also relevant, but they're a separate thing called containers.

A video file typically has encoded audio, encoded video, and metadata about the video file. A file format like MP4 describes a specific format for storing the encoded audio and video data, as well as the metadata.

Video Container

Video compression software stores the encoded audio/video and metadata into a file according to the file format / specs. This is called muxing.

Likewise, video players follow the file format specs to read the metadata and find the encoded audio/video. This is called demuxing.

When compressing a video file, you need to both encode it and mux it (in that order). These are two separate stages of the process. Likewise, when playing a video file, you need to both demux it and then decode it (in that order).

When a video player opens, say, an mp4 file, the logic flow is as follows:

  • Ok, the file ends in .mp4, so it must be an mp4 file. Let me load the library for parsing mp4 files, and parse then parse file.

  • Great, I've parsed the mp4 file, I now have the metadata and know where in the byte offsets are to fetch the encoded audio and video.

  • I'll start fetching the first encoded video frames, decode them, and start displaying the decoded video frame to the user.

If you ever see a "video file is corrupt" message from a video player, it's likely that the video file doesn't follow the file format spec and there was an error while trying the parse / demux the video.

What is WebCodecs?

Now that we've covered codecs, let's put them on the Web.

WebCodecs = Web + Codecs

WebCodecs is an API that allows frontend developers to encode and decode video in the browser efficiently (using hardware acceleration), and with very low level control (encode/decode on a frame by frame basis).

The hardware acceleration bit is important, as you can't just poly fill or re-implement the API yourself. WebCodecs gives direct access to specialized hardware for encoding/decoding, making it as performant as a desktop video app.

Before WebCodecs

It's worth taking a moment to understand why WebCodecs exists. Before the WebCodecs API existed, there were several alternatives you could use for video operations in the browser.

  • HTMLVideoElement: You can still create a element and use it for decoding a video. It's easy to use, but you lack frame level control. Your only control is setting the 'video.currrentTime' property and waiting for it to seek, often leading to dropped/missing frames.

  • Media Recorder API: Essentially allows you to 'screen record' any canvas element or video stream. While it works, it's functionally equivalent to screen recording Adobe Premeire pro instead of clicking render. For editing scenarios, you lose frame level control and can only process video at real-time speed.

  • FFMPEG.js: A port of the popular video processing tool ffmpeg, which runs ffmpeg in the browser. Many tools used this in the past, but it lacks hardware acceleration, making it much slower than WebCodecs. It also has file size restrictions stemming from the fact that it runs in WebAssembly, making it difficult to work with videos that are larger than 100 MB.

WebCodecs was built and released in 2021 to enable low-level, hardware accelerated video decoding and encoding. It's great for high-performance streaming and video editing, which were use cases not well-served by the existing APIs.

Core API

The core API for WebCodecs consists of two new "data types", the VideoFrame and EncodedVideoChunk, as well as the VideoEncoder and VideoDecoder interfaces.

VideoFrame

The Javascript VideoFrame object conceptually contains both pixel data and metadata about the video frame.

VideoFrame object

You can actually create a new VideoFrame object from any image source, as long as you include the metadata:

const bitmapFrame = new VideoFrame(imgBitmap, {timestamp: 0});

const imageFrame = new VideoFrame(htmlImageEl, {timestamp: 0});

const videoFrame = new VideoFrame(htmlVideoEl, {timestamp: 0});

const canvasFrame = new VideoFrame(canvasEl, {timestamp: 0});

For a video editing app, for example, you would typically perform image editing operations on each frame on a canvas, and then you would grab each VideoFrame from the canvas.

You can also draw a VideoFrame to a canvas using the Canvas 2D rendering context:

ctx.drawImage(frame, 0, 0);

You would typically do this when rendering / playing back a video in the browser.

EncodedVideoChunk

An EncodedVideoChunk is just the compressed version of a VideoFrame, containing the binary data as well as the same metadata as the frame.

EncodedVideoChunk

You would typically get EncodedVideoChunks from a library which extracts them from a File object.

import { getVideoChunks } from 'webcodecs-utils'

const chunks = <EncodedVideoChunk[]> await getVideoChunks(<File> file);

Alternatively, it's the output you get from a VideoEncoder object.

There's not much useful stuff you can do with EncodedVideoChunks – it's just the binary data that you read from files, write to files, or stream over the internet.

Video streaming with encode and decode

The value in EncodedVideoChunk is that it's ~100x smaller than raw video data, which is why you'd send EncodedVideoChunks instead of raw video when streaming (and writing to a file).

VideoEncoder

A VideoEncoder turns VideoFrame objects into EncodedVideoChunk objects.

VideoEncoder

The core API looks something like this, where you define the callback where the VideoEncoder returns EncodedVideoChunk objects.

const encoder = new VideoEncoder({
    output: function(chunk: EncodedVideoChunk, meta: any){
        // Do something with the chunk
    },
    error: function(e: any)=> console.warn(e);
});

Keep in mind that this is an async process, and not even a typical async process. You can't just treat this as a per-frame operation.

// Does not work like this
const frame  = await encoder.encode(chunk);

This is because of how video encoding actually works under the hood. So you have to accept that the outputs are returned via callback, and you get the outputs when you get them.

Once you define your encoder, you can then configure the VideoEncoder with your choice of codec (we'll get to this), as well as other parameters like width, height, framerate and bitrate.

encoder.configure({
    'codec': 'vp9.00.10.08.00', // We'll get to this
     width: 1280,
     height: 720,
     bitrate: 1000000 //1 MBPS,
     framerate: 25
});

You can then start encoding frames. Here we assume we already have VideoFrame objects, and we make every 60th frame a Key Frame.

for (let i=0; i < frames.length; frames++){
    encoder.encode(frames[i], {keyFrame: i%60 ==0})
}

VideoDecoder

The Video Decoder does the reverse, turning EncodedVideoChunk objects into VideoFrame objects.

VideoDecoder

Here's a simplified example of how to set up the VideoDecoder. First, extract the EncodedVideoChunk objects and the decoder config from the video file. Here, we don't choose the config – the config was chosen by whoever encoded the file. When decoding, we extract the config from the file.

import { demuxVideo } from 'webcodecs-utils';

const {chunks, config} = await demuxVideo(<File> file);

Next, we set up the VideoDecoder by specifying the callback when VideoFrame objects are generated, and we configure it with the config.

const decoder = new VideoDecoder({
    output: function(frame: VideoFrame){
        //do something with the VideoFrame
    },
    error: function(e: any)=> console.warn(e);
});

decoder.configure(config)

Again, like with VideoEncoder, it returns frames in a callback. Finally we can start decoding chunks.

for (const chunk of chunks){
    decoder.decode(chunk);
}

Putting it all together

At its core, the WebCodecs API is just the two data types (EncodedVideoChunk, VideoFrame) and the VideoEncoder and VideoDecoder interfaces which convert between the two data types.

The core of WebCodecs

Keep in mind that the WebCodecs API doesn't actually work with video files. It only applies the encoding and decoding, and EncodedVideoChunk objects just represent binary data.

Reading video files and writing video files are their own, separate thing called muxing/demuxing.

Muxing and Demuxing

To write to a video file, you'll also need to mux the video. And to play a video file, you need to demux the video. This involves following the file format of the video container, parsing the video file (in the case of demuxing), or placing encoded video data in the right place in the file you are writing to (muxing).

Muxing and Demuxing are not included in the WebCodecs API, so you'll need to use a separate library to handle muxing and demuxing.

Demuxing

To play a video back in the browser, we need to both demux the video and decode the video, in that order.

Demuxing and decoding

There are several libraries you can use to demux videos, including MediaBunny or web-demuxer. For the purposes of this tutorial, I put a very simplified wrapper around these libraries and exposed it in the webcodecs-utils package, so that demuxing is a very simple 2-liner:

import { demuxVideo } from 'webcodecs-utils'
const {chunks, config} = await demuxVideo(file);

This reads the entire video into memory, so don't do this in practice. But it's helpful in making a simple, readable hello world for WebCodecs.

The following snippet will take in a video file (File object), decode it, and paint the result to a canvas. Here, we get the frames from the output callback, and run the draw calls directly from the callback.

import { demuxVideo } from 'webcodecs-utils'

async function playFile(file: File){

    const {chunks, config} = await demuxVideo(file);
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    const decoder = new VideoDecoder({
        output(frame: VideoFrame) {
            ctx.drawImage(frame, 0, 0);
            frame.close()
        },
        error(e) {}
    });


    decoder.configure(config);

    for (const chunk of chunks){
        decoder.decode(chunk)
    }

}

Here's our super barebones demo for playing back an actual video:

For a more 'correct' demuxing example, here is what demuxing looks like with MediaBunny, where you can extract chunks in an iterative fashion.

import { EncodedPacketSink, Input, ALL_FORMATS, BlobSource } from 'mediabunny';

const input = new Input({
  formats: ALL_FORMATS,
  source: new BlobSource(<File> file),
});

const videoTrack = await input.getPrimaryVideoTrack();
const sink = new EncodedPacketSink(videoTrack);

for await (const packet of sink.packets()) {
  const chunk = <EncodedVideoChunk> packet.toEncodedVideoChunk();
}

Muxing

To write a video file, you not only need to encode it (with the VideoEncoder) you also need to mux it. This involves taking the encoded chunks and placing them in the right place in the output binary file that you're writing to.

Muxing and Encoding

Again, you need a library to mux videos ( MediaBunny), but for demo purposes I created a super simple wrapper. Here we define a super basic ExampleMuxer.

import { ExampleMuxer } from 'webcodecs-utils'

const muxer = new ExampleMuxer('video');

for (const chunk of encodedChunks){
    muxer.addChunk(chunk);
}

const outputBlob = await muxer.finish();

As a full encoding + muxing demo, we'll create an encoder, and we'll set it to mux the output encoded chunks as soon as they are returned.

const encoder = new VideoEncoder({
    output: function(chunk, meta){
        muxer.addChunk(chunk, meta);
    },
    error: function(e){}
})

encoder.configure({
    'codec': 'avc1.4d0034', // We'll get to this
     width: 1280,
     height: 720,
     bitrate: 1000000 //1 MBPS,
     framerate: 25
});

We'll then define a canvas animation, which will draw the current frame number to the screen, just to prove it's working.

const canvas = new OffscreenCanvas(640, 360);
const ctx = canvas.getContext('2d');
const TOTAL_FRAMES=300;
let frameNumber = 0;
let chunksMuxed = 0;
const fps = 30;


function renderFrame(){
    ctx.fillStyle = '#000';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = 'white';
    ctx.font = `bold ${Math.min(canvas.width / 10, 72)}px Arial`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(`Frame ${frameNumber}`, canvas.width / 2, canvas.height / 2);
}

Finally we'll create the encode loop, which will draw the current frame, and then encode it.


let flushed = false;

async function encodeLoop(){

    renderFrame();

    const frame = new VideoFrame(canvas, {timestamp: frameNumber/fps*1e6});
    encoder.encode(frame, {keyFrame: frameNumber %60 ===0});
    frame.close();

    frameNumber++;

    if(frameNumber === TOTAL_FRAMES) {
        if (!flushed) encoder.flush();
    }
    else return requestAnimationFrame(encodeLoop);
}

Putting it all together, you can encode the canvas animation to a video file with frame-level accuracy.

You can download the video and use any video inspection tool to verify that every single frame number is included.

Videos with frame level accuracy

This is one of the critical distinctions that separates this from other web APIs like MediaRecorder which can also encode video, but has no frame-level accuracy. WebCodecs makes sure that you can control and guarantee the consistency of each frame.

Finally, a proper full, muxing example using MediaBunny would look like this:

import {
  EncodedPacket,
  EncodedVideoPacketSource,
  BufferTarget,
  Mp4OutputFormat,
  Output
} from 'mediabunny';

async function muxChunks(chunks: EncodedVideoChunk[]): Promise <Blob>{

    const output = new Output({
        format: new Mp4OutputFormat(),
        target: new BufferTarget(),
    });

    const source = new EncodedVideoPacketSource('avc');
    output.addVideoTrack(source);

    await output.start();

    for (const chunk of chunks){
        source.add(EncodedPacket.fromEncodedChunk(chunk))
    }

    await output.finalize();
    const buffer = <ArrayBuffer> output.target.buffer;
    return new Blob([buffer], { type: 'video/mp4' });

});

Building a Video Converter Utility

Now that we've covered the basics of WebCodecs as well as Muxing, we'll move towards actually building an MVP of something useful: a video converter utility. We'll be able to use it to convert between mp4 and webm, and do some basic operations like resizing and flipping the video.

Transcoding

Before we do resizing and flipping, let's first handle a basic conversion decoding a video, and encoding the video to a new format. This is called transcoding.

To transcode video, we need to set up a pipeline with the following processes:

  • Demuxing: Read EncodedVideoChunks from a video file

  • Decoding: Convert EncodedVideoChunks to VideoFrames

  • Encoding: Convert VideoFrames to new EncodedVideoChunks

  • Muxing: Write the EncodedVideoChunks to a new video file

Our pipeline looks something like this:

Transcoding pipeline

Using everything we've covered in this article up until now, we could build a full working demo with just VideoEncoder and VideoDecoder as discussed. But then state management and tracking frames becomes complicated and error prone.

We're going to add one more abstraction, using the Streams API, which will make our pipeline look like the below. It ties directly to our mental model of our pipeline and simplifies a ton of details like state management.

const transcodePipeline = demuxerReader
    .pipeThrough(new VideoDecoderStream(videoDecoderConfig))
    .pipeThrough(new VideoEncoderStream(videoEncoderConfig))
    .pipeTo(createMuxerWriter(muxer));

await transcodePipeline;

To do this, we'll create a TransformStream for the VideoDecoder and VideoEncoder.

class VideoDecoderStream extends TransformStream<{ chunk: EncodedVideoChunk; index: number }, { frame: VideoFrame; index: number }> {
  constructor(config: VideoDecoderConfig) {
    let pendingIndices: number[] = [];
    super(
      {
        start(controller) {
          decoder = new VideoDecoder({
            output: (frame) => {
              const index = pendingIndices.shift()!;
              controller.enqueue({ frame, index });
            },
            error: (e) => controller.error(e),
          });

          decoder.configure(config);
        },

        async transform(item, controller) {
          pendingIndices.push(item.index);
          decoder.decode(item.chunk);
        },

        async flush(controller) {
          await decoder.flush();
          if decoder.state !== 'closed' decoder.close();
        },
      }
    );
  }
}

I won't bore you with the full code, but I've packaged these utilities in the webcodecs-utils package, which can be used as such:

import {
  SimpleDemuxer,
  VideoDecodeStream,
  VideoEncodeStream,
  SimpleMuxer,
} from "webcodecs-utils";

Our code for transcoding a file then becomes this:

const demuxer = new SimpleDemuxer(videoFile);
await demuxer.load();
const decoderConfig = await demuxer.getVideoDecoderConfig();

const encoderConfig = {/*Whatever we decide*/};

// Set up muxer
const muxer = new SimpleMuxer({ video: "avc" });

// Build the upscaling pipeline
await demuxer.videoStream()
  .pipeThrough(new VideoDecodeStream(decoderConfig))
  .pipeThrough(new VideoEncodeStream(encoderConfig))
  .pipeTo(muxer.videoSink());

// Get output
const blob = await muxer.finalize();

For this intermediate demo, just to actually get transcoding to work, we'll download a pre-built file, and we'll introduce a toggle to output an mp4 file (using h264) or a webm file (using vp9).

We'll use avc1.4d0034 for h264 (most widely supported h264 codec string) and vp09.00.40.08.00 for vp9 (most widely supported vp9 string).

Here's a basic transcoding demo on CodePen:

Transformations

If we want to do any kind of transformations to the video, like flips, crops, rotations, resizing, and so on, we can't just work with pure VideoFrame objects.

The simplest way to accomplish this would be to introduce a Canvas element, where we'll use a 2d Canvas Context to manipulate our source frame and draw that to a canvas.

const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext('2d');

// Very easy to do transformations
ctx.drawImage(sourceFrame, 0, 0);

We'll then use the Canvas as a source image for our output video frame.

const outFrame = new VideoFrame(canvas, {timestamp: sourceFrame.timestamp});

To apply a resize operation, we'll first set the canvas dimensions to our output height and width.

const canvas = new OffscreenCanvas(outputWidth, outputHeight);
const ctx = canvas.getContext('2d');

// Resize sourceFrame to fit output dimensions
ctx.drawImage(sourceFrame, 0, 0, outputWidth, outputHeight);

To apply a horizontal flip operation with canvas2d, we can do the following:

ctx.scale(-1, 1);
ctx.translate(-outputWidth, 0);
ctx.drawImage(sourceFrame, 0, 0, outputWidth, outputHeight);

You can create a full render function that applies these transformations which looks like this:

function render(videoFrame, outW, outH, flipped) {

  canvas.width  = outW;
  canvas.height = outH;

  if (flipped) {
    ctx.scale(-1, 1);
    ctx.translate(-outW, 0);
  }
  ctx.drawImage(videoFrame, 0, 0, outW, outH);

}

Here's an interactive demo of what these transformations look like:

Transform Pipeline

With these transformations, we need to adjust our pipeline to include a transformation step. It will take in a VideoFrame, apply the transforms, and return a transformed frame.

Transcoding pipeline with transforms

In the webcodecs-utils package, there is a VideoProcessStream object for this purpose, which takes in an async function which takes in a VideoFrame and returns a VideoFrame:

import { VideoProcessStream} from "webcodecs-utils";
 
new VideoProcessStream(async (frame) => {
      // Apply transformations
      return procesedFrame;
    }),

So to apply our transformations, we can set it up as so:

import { VideoProcessStream} from "webcodecs-utils";
 

const canvas = new OffscreenCanvas(outW, outH);
const ctx = canvas.getContext('2d');

const processStream = new VideoProcessStream(async (frame) => {
  
  if (flipped) {
    ctx.scale(-1, 1);
    ctx.translate(-outW, 0);
  }
  ctx.drawImage(frame, 0, 0, outW, outH);

  return new VideoFrame(canvas, {timestamp: frame.timestamp});

});

And then our full pipeline looks like this:

const demuxer = new SimpleDemuxer(videoFile);
await demuxer.load();
const decoderConfig = await demuxer.getVideoDecoderConfig();

const encoderConfig = {/*Whatever we decide*/};

// Set up muxer
const muxer = new SimpleMuxer({ video: "avc" });

// Build the upscaling pipeline
await demuxer.videoStream()
  .pipeThrough(new VideoDecodeStream(decoderConfig))
  .pipeThrough(processStream) // Just defined this
  .pipeThrough(new VideoEncodeStream(encoderConfig))
  .pipeTo(muxer.videoSink());

// Get output
const blob = await muxer.finalize();

Here's a full working demo with the process pipeline:

Complete Demo

Now, for the complete tool, we'll make some key changes:

  • You can upload your own video

  • We'll preview the transformations by extracting a frame

  • We'll add progress measurement

For the input, that's trivial:

<input type="file" onchange="handler(event)" />

For frame previews, we could use WebCodecs to generate a preview, but because the preview doesn't need frame-level accuracy or high performance, it's easier to just use the HTML5 VideoElement to grab a video frame from the source file.

async function getFirstFrame(file) {
  const video = document.createElement("video");
  video.src = URL.createObjectURL(file);
  video.muted = true;

  await new Promise((resolve) => video.addEventListener("loadeddata", resolve, { once: true }));
  video.currentTime = 0;
  await new Promise((resolve) => video.addEventListener("seeked", resolve, { once: true }));

  return new VideoFrame(video, {timestamp: 0});
}

Finally, we can calculate progress in the process function by using the frame timestamp / the video duration.

const {duration} = await demuxer.getMediaInfo();


const processStream = new VideoProcessStream(async (frame) => {
  
  if (flipped) {
    ctx.scale(-1, 1);
    ctx.translate(-outW, 0);
  }
  ctx.drawImage(frame, 0, 0, outW, outH);

   // Frame timestamps are in microseconds, duration in seconds
  const progress = frame.timestamp/(duration*1e6); 

  return new VideoFrame(canvas, {timestamp: frame.timestamp});

});

Putting this all together, we can finally put together a full working video converter utility:

And that's it! We've built an MVP of something actually useful with WebCodecs 🎉, with Demuxing, Decoding, Canvas Transforms, Encoding, and Muxing.

The only difference between this and a full-fledged browser editing suite like Capcut is the scale and scope of transformations. But the video processing logic would be nearly identical.

Production Concerns

It's great that we've been able to create something useful, but before we wrap up, it's important to cover some production-level concerns.

Codecs

You might have noticed strings like vp09.00.10.08 in the demos, but I glossed over the details. We'll cover that now:

First, WebCodecs works with specific codec strings like vp09.00.10.08, not just 'vp9'. The following won't work:

const codec = VideoEncoder({
    codec: 'vp9', //This won't work!
    //...
})

As discussed previously, when decoding video, you don't really get a choice of codec. The video is already encoded, and so you need to get the codec from the video, as shown in the previous demos.

The demuxing libraries mentioned will identify the correct codec string, so you don't need to worry about that.

const decoderConfig = await demuxer.getVideoDecoderConfig();
//decoderConfig.codec = exact codec string for the video

When encoding a video, you can can choose your codec. Some people care a lot about codec choice, but from a very practical, pragmatic perspective, these rules of thumb should work for most developers:

  • If the videos your app generates will be downloaded by users and/or you want to output mp4 files, use h264.

  • If the videos generated are for internal use or you control video playback, and you don't care about format, use vp9 with webm (open source, better compression, most widely supported codec).

  • For most apps, these two options will cover you — deeper codec selection is a rabbit hole you don't need to go down yet.

Once you have a codec family chosen, you need to choose a specific codec string such as avc1.42001f.

The other numbers in the string specify certain codec parameters which are not as important from a developer perspective. If your goal is maximum compatibility, here's your cheat sheet for what codec strings to use

h264 (for mp4 files)
  • avc1.42001f - base profile, most compatible, supports up to 720p (99.6% support)

  • avc1.4d0034 - main profile, level 5.2 (supports up to 4K) (98.9% support)

  • avc1.42003e - base profile, level 6.2 (supports up to 8k) (86.8% support)

  • avc1.64003e - high profile - level 6.2 (supports up to 8k) (85.9% support)

vp9 (for webm files)

You can also use the getCodecString function from the webcodecs-utils package:

import { getCodecString } from 'webcodecs-utils'

const codec_string = getCodecString('vp9', width, height, bitrate)

You can find a comprehensive list of what codecs and codec strings you can use in WebCodecs here.

Bit rate

On top of height and width (which you presumably know from your content) and a codec string (which we just discussed), you also need to specify a bit rate when encoding video.

Video Compression algorithms have a trade-off between quality and file size. You can have high quality video with big file sizes, or lower quality video with lower file sizes.

Here's a quick visualization of what different quality levels look like for a 1080p video encoded at different bit rates:

300 kbps

300kbps frame

1 Mbps

1Mbps frame

3 Mbps

3 Mbps frame

10 Mbps

10 Mbps frame

Here's a quick lookup table for bitrate guidance:

Resolution Bitrate (30fps) Bitrate (60fps)
4K 13-20 Mbps 20-30 Mbps
1080p 4.5-6 Mbps 6-9 Mbps
720p 2-4 Mbps 3-6 Mbps
480p 1.5-2 Mbps 2-3 Mbps
360p 0.5-1 Mbps 1-1.5 Mbps
240p 300-500 kbps 500-800 kbps

You can also use this utility function in your own app as a quick approximation:

function getBitrate(width, height, fps, quality = 'good') {
    const pixels = width * height;

    const qualityFactors = {
      'low': 0.05,
      'good': 0.08,
      'high': 0.10,
      'very-high': 0.15
    };

    const factor = qualityFactors[quality] || qualityFactors['good'];

    // Returns bitrate in bits per second
    return pixels * fps * factor;
  }

The same function is also available in the webcodecs-utils package:

import { getBitrate } from 'webcodecs-utils'

GPU vs CPU

Most user devices have some type of graphics card (typically called integrated graphics). These are specialized chips with specific silicon architectures optimized for encoding and decoding video, as well as for basic graphics.

You might hear "GPU" and think AI data centers and gamers. But as far as web applications are concerned, almost everyone has a GPU.

This is important because while most frontend-development almost exclusively deals with the CPU, WebCodecs and video processing work primarily on the GPU.

Here's a quick guide for what kind of data is stored where:

Data Type Location
VideoFrame GPU
EncodedVideoChunk CPU
ImageBitmap GPU
ArrayBuffer CPU
File CPU + Disk

There's a performance cost to moving data around, and this also becomes important for managing memory.

Memory

VideoFrame objects can be quite large – 30MB for a 4K video. A user's graphics card typically reserves some portion of RAM for "Video Memory" or "VRAM" which is where VideoFrame objects would be stored.

So if a user has 8GB of RAM, they would typically have 2GB of VRAM (how much is decided by the operating system).

If the amount of video data exceeds VRAM, your application will crash. This means that for a typical user, if you have more than 67 4K frames in memory (~2 seconds of video) the program will crash.

When VideoFrames are generated

VideoFrame objects are generated whenever you create a new VideoFrame(source) but also from the VideoDecoder, specifically the output callback. Every time a frame is generated, memory usage goes up.

How to remove VideoFrames

You can't rely on standard garbage collection for VideoFrame objects. You have to explicitly call close() on a frame when you're done:

frame.close()

In the Streams/Pipeline code and demo showed earlier, frames are actually being closed as soon as they are encoded in the VideoProcessStream and VideoEncodeStream interfaces.

The other reason Streams are helpful for WebCodecs is the highWaterMark property, which defaults to 10. What this means is that when you run:

await demuxer.videoStream()
  .pipeThrough(new VideoDecodeStream(decoderConfig))
  .pipeThrough(processStream) 
  .pipeThrough(new VideoEncodeStream(encoderConfig))
  .pipeTo(muxer.videoSink());

You ensure that no more than 10 video frames are in memory at any given time. The Streams API allows you to specify that limit while the browser itself deals with the logic of how to make that happen.

If you don't use the Streams API, you'll need to make sure you manage keeping track of memory limits and number of open video frames yourself.

Further Resources

Through this article we've gone over the basics of video processing, introduced the core concepts of the WebCodecs API, and built an MVP of a video converter utility. This is one of the simplest possible demos which actually touches all parts of the API. We also covered some basic production concerns.

This is just an introduction, and only scratches the surface of WebCodecs. For how simple the API looks, building a proper, production-ready WebCodecs application requires moving beyond hello-world demos.

To learn more about WebCodecs, you can check out MDN and the WebCodecsFundamentals, a comprehensive online textbook going much more in depth on WebCodecs.

You can also examine the source code of existing, production tested apps like Remotion Convert (source code) which is most similar to the demo app we covered, and Free AI Video Upscaler (source code, processing pipeline) which is the inspiration for the design patterns presented here and implemented in webcodecs-utils.

Finally, while WebCodecs is harder than it looks, you can make your life a lot easier by using a library like MediaBunny, which simplifies a lot of the details of things like memory management, file I/O, and other details. I use it in my own production WebCodecs applications.

Whether or not you actually build a full, production grade WebCodecs application, you now at least know that it's an option – one that's relatively new, provides better UX with lower server costs, and which is increasingly being adopted by prominent video applications like Capcut and Descript for its benefits.



Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

What's new for the Microsoft Fluent UI Blazor library 5.0 RC2

1 Share

We just shipped the second release candidate for v5, and boy did we manage to squeeze in a slew of new stuff...

Since RC1, we’ve worked hard on delivering two new components — AutoComplete and Toast — along with a powerful Theme API (including a Theme Designer), major DataGrid enhancements such as pinned columns, and dozens of improvements across the board.

AutoComplete

The FluentAutocomplete component brings a completely rebuilt, fully-featured AutoComplete experience to v5, replacing the v4 implementation with a more modern and better designed component.

Key capabilities:

  • Search-as-you-type — Filter options dynamically as the user types, with built-in debounce support.
  • Multi-select — Select multiple items displayed as dismissible badges inside the input area.
  • Keyboard navigation — Full arrow-key navigation, Enter to select, Escape to close, and Backspace to remove the last selected item.
  • Custom option templates — Use OptionTemplate to render rich, custom content for each suggestion.
  • Progress indicator — Show a loading indicator while fetching results asynchronously.
  • MaxAutoHeight / MaxSelectedWidth — Control the layout and overflow behavior of selected items.

Toast & ToastService

The new FluentToast service provides a feature-complete experience, including support for live-updating progress toasts.

The library supports four toast scenarios through ToastType:

  • Communication — General notifications and messages
  • Confirmation — Success / failure confirmations
  • IndeterminateProgress — Long-running operations without progress tracking
  • DeterminateProgress — Operations with measurable progress (e.g. upload)

Simple usage:

@inject IToastService ToastService

// Fire-and-forget
await ToastService.ShowToastAsync(options =>
{
    options.Title = "File saved",
    options.Intent = ToastIntent.Success,
    Timeout = 3000,
});

Advanced: Use a live toast instance

For scenarios like uploads or long-running operations, use ShowToastInstanceAsync to get a live instance reference. You can then update the content of the Toast while it is being shown:

var toast = await ToastService.ShowToastInstanceAsync(options =>
{
    options.Title = "Uploading document...";
    options.Type = ToastType.DeterminateProgress;
});

// Update progress while visible
await toast.UpdateAsync(t => t.Progress = 50);

// Complete and dismiss
await toast.UpdateAsync(t => t.Progress = 100);
await toast.CloseAsync();

Other highlights

  • Queueing — The FluentToastProvider manages maximum visible toasts, queue promotion, and positioning.
  • Pause on hover — Toast timeout pauses when the user hovers over it, or when the browser window loses focus.
  • Animated transitions — Smooth open/close animations.
  • Accessibility — ARIA attributes and politeness levels are applied based on toast intent.

Theme API & Designer

We're introducing a comprehensive Theme API that gives you full control (within the bounds of the Fluent Design System) over your application’s visual identity — from a simple brand color to a fully customized design token set, with built-in persistence and a live Theme Designer.

Set the brand color declaratively

Add data-theme and data-theme-color attributes to yourtag:

<body data-theme="light" data-theme-color="#0078D4">

The library automatically detects these attributes, generates a color ramp, and applies it to the application.

Set the brand color with code

A full API is available through the IThemeService. A simple example on how to use this:

@inject IThemeService ThemeService

// Set a custom brand color
await ThemeService.SetThemeAsync("#6B2AEE");

// Switch to dark mode
await ThemeService.SetDarkThemeAsync();

// Toggle light ↔ dark
await ThemeService.SwitchThemeAsync();

// Apply the Teams theme
await ThemeService.SetTeamsLightThemeAsync();

// Full control with settings
await ThemeService.SetThemeAsync(new ThemeSettings
{
    Color = "#6B2AEE",
    Mode = ThemeMode.Dark,
    HueTorsion = 0.1f,
    Vibrancy = 0.2f,
});

Theme Designer

The demo site includes a Theme Designer page where you can interactively pick a brand color, adjust hue torsion and vibrancy, preview the generated color ramp, and see your theme applied to actual components in real time. When you’re happy with the result, you can apply the settings to the demo site with a click on a button.

Key features

  • Brand color ramp — Automatic generation of a full color ramp from a single hex color (with the option to use the exact specified color!)
  • Light / Dark / System — Support for all three modes, with automatic system preference detection
  • Teams themes — Built-in Teams Light and Teams Dark themes
  • localStorage — Theme settings are cached and restored across sessions automatically
  • Per-element theming — Apply a custom theme to a specific ElementReference without changing global settings
  • RTL support — SwitchDirectionAsync() to toggle between LTR and RTL

DataGrid Enhancements

The FluentDataGrid has been significantly enhanced in RC2

Pinned (frozen/sticky) columns

Columns can now be pinned to the left or right edge of the grid, so they remain visible during horizontal scrolling:

<FluentDataGrid Items="@people" Style="overflow-x: auto; max-width: 800px;">
    <PropertyColumn Property="@(p => p.Id)" Width="80px" Pin="DataGridColumnPin.Left" />
    <PropertyColumn Property="@(p => p.Name)" Width="200px" Pin="DataGridColumnPin.Left" />
    <PropertyColumn Property="@(p => p.Email)" Width="300px" />
    <PropertyColumn Property="@(p => p.City)" Width="200px" />
    <PropertyColumn Property="@(p => p.Country)" Width="200px" />
    <PropertyColumn Property="@(p => p.Actions)" Width="100px" Pin="DataGridColumnPin.Right" />
</FluentDataGrid>

Pinned columns require an explicit width and must be contiguous (all start-pinned columns must come first, all end-pinned columns must come last). The DataGrid validates these rules and throws a descriptive exception when detecting an invalid configuration.

HierarchicalSelectColumn

Besides the hierarchical DataGrid option added in RC!, a new column type has now been added that provides parent-child selection behavior, allowing users to select groups of related rows through a hierarchical checkbox.

And more

  • StripedRows parameter for alternating row styling
  • DisableCellFocus parameter
  • OnSortChanged event callback
  • Skip debounce delay on first provider call when using Virtualize
  • Fix SelectedItems getting unselected when using pagination/virtualization
  • Some Width issues fixed

Calendar & DatePicker

We've added MinDate and MaxDate parameters so it is now possible to constrain the selectable date range:

<FluentCalendar @bind-Value="@selectedDate"
                MinDate="@DateTime.Today"
                MaxDate="@DateTime.Today.AddMonths(3)" />

Other changes include:

  • Year view: current year centered — The year picker now places the current year in the middle row for better usability.
  • Fix: month/year navigation getting stuck — Resolved an issue where clicking month or year could leave the calendar in a stuck state.
  • Width forwarded — The Width parameter is now properly forwarded to the underlying FluentTextInput.

Other component improvements

Some other noteworthy improvements and fixes:

  • FluentLink — Support clickable links with OnClick events and improved hover styles
  • Badge — Added .fluent-badge CSS classes for custom styling
  • AppBar — Allow hiding active bar, render active bar when horizontal
  • AppBar — Calculate height of active bar dynamically
  • Nav — Enhanced accessibility (a11y) support
  • Nav — Refactoring and issue fixes
  • DragContainer/DropZone — Switch from Action to EventCallback for event handlers
  • Checkbox — Fix “checked” logic to respect ThreeState parameter
  • Accordion — Fix change event to only trigger for fluent-accordion elements
  • List — Refactor OptionSelectedComparer to use IEqualityComparer
  • Placeholder — Fix placeholder rendering error
  • Custom events — Rename custom events to avoid .NET 11 exception

Localization

Building on the localization system we introduced in RC1:

  • Paginator localizable strings — All Paginator strings (page labels, navigation buttons) are now localizable through IFluentLocalizer.
  • Translation key-value pairs reference — The documentation now includes a complete table of all default translation keys and their values, making it easier to implement IFluentLocalizer.

MCP Server & Tooling

  • Migration Service for v4 → v5 — A new migration service and resources to help automate the transition from v4 to v5 with the MCP Server.
  • ModelContextProtocol updated to v1.1.0 — The MCP Server now uses the latest MCP protocol version.
  • AI Skills docs and download UI — Documentation for the AI Skills available through the library, with a download UI.
  • Version compatibility tools — New tools and documentation to check version compatibility across the Fluent UI Blazor packages family.

Help us and try it now

We are still in the Release Candidate phase. The APIs are mostly stabilized, but we would still very much like the community to help us identify any remaining issues before the final release. Please file issues on GitHub, and don’t hesitate to contribute.

To see what we still want to complete before the v5 final release, see our dev-v5 - TODO List

Thank you to everyone who has already contributed, tested, and provided feedback. We hope you will continue doing so.

A special shout-out goes to the community contributors who made significant contributions to this release!

Read the whole story
alvinashcraft
17 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

The Insert Benchmark vs MariaDB 10.2 to 13.0 on a 32-core server

1 Share

This has results for MariaDB versions 10.2 through 13.0 vs the Insert Benchmark on a 32-core server. The goal is to see how performance changes over time to find regressions or highlight improvements. My previous post has results from a 24-core server.  Differences between these servers include:

  • RAM - 32-core server has 128G, 24-core server has 64G
  • fsync latency - 32-core has an SSD with high fsync latency, while it is fast on the 24-core server
  • sockets - 32-core server has 1 CPU socket, 24-core server has two
  • CPU maker  - 32-core server uses an AMD Threadripper, 24-core server has an Intel Xeon
  • cores - obviously it is 32 vs 24, Intel HT and AMD SMT are disabled

The results here for modern MariaDB are great for the CPU-bound workload but not for the IO-bound workload.. They were great for both on the 24-core server. The regressions are likely caused by the extra fsync calls that are done because the equivalent of equivalent of innodb_flush_method =O_DIRECT_NO_FSYNC was lost with the new options that replace innodb_flush_method starting in MariaDB 11.4. I created MDEV-33545 to request support for it. The workaround is to use an SSD that doesn't have high fsync latency, which is always a good idea, but not always possible.

tl;dr

  • for a CPU-bound workload
    • the write-heavy steps are much faster in 13.0.0 than 10.2.30
    • the read-heavy steps get similar QPS in 13.0.0 and 10.2.30
    • this is similar to the results on the 24-core server
  • for an IO-bound workload
    • the initial load (l.i0) is much faster in 13.0.0 than 10.2.30
    • the random write step (l.i1) is slower in 13.0.0 than 10.2.30 because fsync latency
    • the range query step (qr100) gets similar QPS in 13.0.0 and 10.2.30
    • the point query step (qp100) is much slower in 13.0.0 than 10.2.30 because fsync latency

Builds, configuration and hardware

I compiled MariaDB from source for versions 10.2.30, 10.2.44, 10.3.39, 10.4.34, 10.5.29, 10.6.25, 10.11.16, 11.4.10, 11.8.6, 12.3.1 and 13.0.0.

The server has 24-cores, 2-sockets and 64G of RAM. Storage is 1 NVMe device with ext-4 and discard enabled. The OS is Ubuntu 24.04. Intel HT is disabled.

The my.cnf files are here for: 10.210.310.410.510.610.1111.411.812.3 and 13.0

For MariaDB 10.11.16 I used both the z12a config, as I did for all 10.x releases, and also used the z12b config. The difference is that the z12a config uses innodb_flush_method =O_DIRECT_NO_FSYNC while the z12b config uses =O_DIRECT. And the z12b config is closer to the configs used for MariaDB because with the new variables that replaced innodb_flush_method, we lose support for the equivalent of =O_DIRECT_NO_FSYNC.

And I write about this because the extra fsync calls that are done when the z12b config is used have a large impact on throughput on a server that uses an SSD with high fsync latency, which causes perf regressions for all DBMS versions that used the z12b config -- 10.11.16, 11.4, 11.8, 12.3 and 13.0.

The Benchmark

The benchmark is explained here and is run with 12 clients with a table per client. I repeated it with two workloads:
  • CPU-bound
    • the values for X, Y, Z are 10M, 16M, 4M
  • IO-bound
    • the values for X, Y, Z are 300M, 4M, 1M
The point query (qp100, qp500, qp1000) and range query (qr100, qr500, qr1000) steps are run for 1800 seconds each.

The benchmark steps are:

  • l.i0
    • insert X rows per table in PK order. The table has a PK index but no secondary indexes. There is one connection per client.
  • l.x
    • create 3 secondary indexes per table. There is one connection per client.
  • l.i1
    • use 2 connections/client. One inserts Y rows per table and the other does deletes at the same rate as the inserts. Each transaction modifies 50 rows (big transactions). This step is run for a fixed number of inserts, so the run time varies depending on the insert rate.
  • l.i2
    • like l.i1 but each transaction modifies 5 rows (small transactions) and Z rows are inserted and deleted per table.
    • Wait for S seconds after the step finishes to reduce variance during the read-write benchmark steps that follow. The value of S is a function of the table size.
  • qr100
    • use 3 connections/client. One does range queries and performance is reported for this. The second does does 100 inserts/s and the third does 100 deletes/s. The second and third are less busy than the first. The range queries use covering secondary indexes. If the target insert rate is not sustained then that is considered to be an SLA failure. If the target insert rate is sustained then the step does the same number of inserts for all systems tested. This step is frequently not IO-bound for the IO-bound workload.
  • qp100
    • like qr100 except uses point queries on the PK index
  • qr500
    • like qr100 but the insert and delete rates are increased from 100/s to 500/s
  • qp500
    • like qp100 but the insert and delete rates are increased from 100/s to 500/s
  • qr1000
    • like qr100 but the insert and delete rates are increased from 100/s to 1000/s
  • qp1000
    • like qp100 but the insert and delete rates are increased from 100/s to 1000/s
Results: overview

The performance reports are here for the CPU-bound and IO-bound workloads.

The summary sections from the performances report have 3 tables. The first shows absolute throughput by DBMS tested X benchmark step. The second has throughput relative to the version from the first row of the table. The third shows the background insert rate for benchmark steps with background inserts. The second table makes it easy to see how performance changes over time. The third table makes it easy to see which DBMS+configs failed to meet the SLA.

Below I use relative QPS to explain how performance changes. It is: (QPS for $me / QPS for $base) where $me is the result for some version. The base version is MariaDB 10.2.30.

When relative QPS is > 1.0 then performance improved over time. When it is < 1.0 then there are regressions. The Q in relative QPS measures: 
  • insert/s for l.i0, l.i1, l.i2
  • indexed rows/s for l.x
  • range queries/s for qr100, qr500, qr1000
  • point queries/s for qp100, qp500, qp1000
This statement doesn't apply to this blog post, but I keep it here for copy/paste into future posts. Below I use colors to highlight the relative QPS values with red for <= 0.95, green for >= 1.05 and grey for values between 0.95 and 1.05.

Results: CPU-bound

The performance summary is here.

The summary per benchmark step, where rQPS means relative QPS.
  • l.i0
    • MariaDB 13.0.0 is faster than 10.2.30, rQPS is 1.47
    • CPU per insert (cpupq) and KB written to storage per insert (wKBpi) are much smaller in 13.0.0 than 10.2.30 (see here)
  • l.x
    • I will ignore this
  • l.i1, l.i2
    • MariaDB 13.0.0 is faster than 10.2.30, rQPS is 1.50 and 1.37
    • CPU per write (cpupq) is much smaller in 13.0.0 than 10.2.30 (see here)
  • qr100, qr500, qr1000
    • MariaDB 13.0.0 and 10.2.30 have similar QPS (rQPS is close to 1.0)
    • CPU per query (cqpq) is similar in 13.0.0 and 10.2.30 (see here)
  • qp100, qp500, qp1000
    • MariaDB 13.0.0 and 10.2.30 have similar QPS (rQPS is close to 1.0)
    • CPU per query (cqpq) is similar in 13.0.0 and 10.2.30 (see here)

Results: IO-bound

The performance summary is here.

The summary per benchmark step, where rQPS means relative QPS.
  • l.i0
    • MariaDB 13.0.0 is faster than 10.2.30, rQPS is 1.25
    • CPU per insert (cpupq) and KB written to storage per insert (wKBpi) are much smaller in 13.0.0 than 10.2.30 (see here)
  • l.x
    • I will ignore this
  • l.i1, l.i2
    • MariaDB 13.0.0 is slower than 10.2.30 for l.i1, rQPS is 0.68
    • MariaDB 13.0.0 is faster than 10.2.30 for l.i2, rQPS is 1.31. I suspect it is faster on l.i2 because it inherits less MVCC GC debt from l.i1 because it was slower on l.i1. So I won't celebrate this result and will focus on l.i1.
    • From the normalized vmstat and iostat metrics I don't see anything obvious. But I do see a reduction in storage reads/s (rps) and storage read MB/s (rMBps). And this reduction starts in 10.11.16 with the z12b config and continues to 13.0.0. This does not occur on the earlier releases that are eable to use the z12a config. So I am curious if the extra fsyncs are the root cause.
    • From the iostat summary for l.i1 that includes average values for all iostat columns, and these are not divided by QPS, what I see a much higher rate for fsyncs (f/s) as well as an increase in read latency. For MariaDB 10.11.16 the value for r_await is 0.640 with the z12a config vs 0.888 with the z12b config. I assume that more frequent fsync calls hurt read latency. The iostat results don't look great for either the z12a or z12b config and the real solution is to avoid using an SSD with high fsync latency, but that isn't always possible.
  • qr100, qr500, qr1000
    • no DBMS versions were able to sustain the target write rate for qr500 or qr1000 so I ignore them. This server needs more IOPs capacity -- a second SSD, and both SSDs needs power loss protection to reduce fsync latency.
    • MariaDB 13.0.0 and 10.2.30 have similar performance, rQPS is 0.96The qr100 step for MariaDB 13.0.0 might not suffer from fsync latency like the qp100 step because it does less read IO per query than qp100 (see rpq here).
  • qp100, qp500, qp1000
    • no DBMS versions were able to sustain the target write rate for qp500 or qp1000 so I ignore them. This server needs more IOPs capacity -- a second SSD, and both SSDs needs power loss protection to reduce fsync latency.
    • MariaDB 13.0.0 is slower than 10.2.30, rQPS is 0.62
    • From the normalized vmstat and iostat metrics there are increases in CPU per query (cpupq) and storage reads per query (rpq) for all DBMS versions that use the z12b config (see here).
    • From the iostat summary for qp100 that includes average values for all iostat columns the read latency increases for all DBMS versions that use the z12b config. I blame interference from the extra fsync calls.
























Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

How To Write An Irresistible Book Blurb In 5 Easy Steps

1 Share

In this post, we tell you how to write an irresistible book blurb in 5 easy steps. We call it SCOPE and it works for any blurb.

Your book blurb must be well-written and compelling to get a reader’s attention. Your blurb will be an important part of your marketing.

To write a good blurb, you have to make it short and exciting, yet relevant. You need to:

  1. Cut out sub-plots.
  2. Add tension to make it dramatic.
  3. Try not to mention more than two characters’ names.
  4. Promise your audience a read they won’t forget.

How To Write An Irresistible Book Blurb In 5 Easy Steps

I’ve come up with this easy acronym to help you create a book blurb. I call it SCOPE. Follow these five pointers and see if it works for you.

SCOPE

Setting
Conflict
Objective
Possible Solution
Emotional Promise

  1. Setting: All stories involve characters who are in a certain setting at a certain time.
  2. Conflict: A good story places these characters in a situation where they have to act or react. A good way to start this part of your blurb is with the words: But, However, Until
  3. Objective: What do your characters need to do?
  4. Possible Solution: Offer the reader hope here. Show them how the protagonist can overcome. Give them a reason to pick up the book. Use the word ‘If’ here.
  5. Emotional Promise: Tell them how the book will make them feel. This sets the mood for your reader.

I am using the film The Edge of Tomorrow to write a blurb using this formula.

Book Blurb Example:

  1. London. The near future. Aliens have invaded Earth and colonised Europe. Major William Cage is a PR expert for the US Army working with the British to stop the invaders crossing the English Channel. Battle after battle is lost until an unexpected victory gives humanity hope.
  2. But a planned push into Europe fails. Cage finds himself in a war he has no way to fight and he is killed. However, he wakes up, rebooted back a day every time he dies.
  3. He lives through hellish day after day, until he finds Sergeant Rita Vrataski who shows him how to fight the enemy.
  4. They painstakingly work out how to destroy the aliens. If they succeed, they will save Earth.
  5. This thrilling, action-packed, science-fiction war story will show you how heroes are made and wars can be won. Against the odds.

You can shorten (or lengthen) this to suit your needs.

SCOPE will work for any blurb. Why don’t you try it on a book you’ve read or a film you’ve seen recently?

If you liked this article, you will enjoy:

  1. How To Write A Query Letter In 12 Easy Steps
  2. How To Write A One-Page Synopsis
  3. 8 Points To Consider When You Name Your Book


by Amanda Patterson
© Amanda Patterson

If you enjoyed this blogger’s writing, read:

  1. How To Write A Play – For Beginners
  2. What Is Metafiction & How Do I Write It?
  3. Fabulous Resources For Crime Writers
  4. What Is A Character Flaw? 123 Ideas For Character Flaws
  5. All About Betrayal In Fiction
  6. The Best Priest Detectives In Fiction
  7. What Is Slipstream Fiction?
  8. What Is Imagery & How Do You Use It In Fiction Writing?
  9. How To Deal With A Writer’s Inner Critic
  10. What Is Exposition In A Story?

The post How To Write An Irresistible Book Blurb In 5 Easy Steps appeared first on Writers Write.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Layoff Thinking

1 Share

LinkedIn has been awash in layoff stories for, God, it feels like forever now. But a recent post got me thinking about layoffs, and how some of our reactions are deeply visceral when others get laid off around us, and why it's such a deeply personal thing to be suddenly unemployed.

First, the post

The video does some interesting analysis around what the real cause of the layoff is (and I think she's right about that), but the text above the video reads, in part:

We are so conditioned to believe that we have no inherent worth in capitalism unless we are EARNING. So we outsource our own worth to the very privileged few who are seemingly doing capitalism "right."

You're worthy, I promise you. Struggle isn't necessary, poverty doesn't happen because you're lazy and entitled.

... which got me thinking. Why is it we take it so hard when we are separated from a company?

In Western (American) society, we often place a great deal of our identity into what we do.

Consider, for a moment, how we greet each other when strangers first meet: "Hi, what's your name? What do you do?" (In the Deep South, I'm told the question is often, "What church do you go to?" while New Yorkers, I'm told, ask "Where ya from?" meaning "Which of the boroughs do you live?" and answers of anything other than a New York borough is essentially discounted and heavily judged.) These questions, right out of the gate, are how we look to understand other people, meaning we are using them to understand that other person.

In essence, these questions are what we use to establish our identity, our sense of selves, and how we represent that self both to others and to ourselves. It's ingrained into us as kids--in fact, it's a natural outgrowth of how, when we are children, we self-identify based on our school/grade/teacher which then leads naturally to college which then leads naturally to employer.

Notice how "What do you do" is right up there, right after the name, even? We use that as a definition of who we are, to ourselves every bit as much as we do to others.

Is it any surprise, then, that people take a layoff hard? Employers are literally striking a hole at somebody's sense of self when laying them off. It's as deep of a blow as taking away their national identity or displacing them out of their culture.

While I've always enjoyed programming and making money as a programmer, I don't think I've had that sense of "self" wrapped entirely in that concept of being a programmer. When everybody around me was a "Java developer" or a ".NET developer", I was just "a developer". Possibly because I've spent a ton of time thinking about all the other things I could (and wanted) to do: fiction author, sommelier, dungeon master, game developer, and a few more to boot. Don't get me wrong, I love coding and I love learning about all this new tech stuff, but if I couldn't make money at it, I'd do it on the side while making money doing whatever else. It's a weird thing to explain sometimes.

My reason for bringing all this up? In the spirit of trying to console people by counseling actions to take: If you're experiencing a layoff, I think it critical to lean into all of the non-work parts of your self. Re-center your sense of identity away from work. Hobbies. Family. Voracious consumer of urban fantasy romance slam poetry. Whatever. Take the chance to rebuild your sense of self around things that aren't work, so that when you get back into work, you're never quite as vulnerable as you were before.

In other words: You are way more than what you do. You have skills, insights, views, and probably a whole lotta love that you can offer. Your company said you have no worth to them? Fuck 'em. You have worth, just because. It sucks, yes, and it's important to grieve. Then get up and go wander the coffee aisle at the local grocery store, enjoying all the smells. Go watch kids in the park for a while. Swing on a swing like you did when you were five. Whatever. Be you. Reconnect with yourself, and realize that nowhere inside you is a company logo. You are waaaaay more than just what you do, and that in of itself is waaaaay bigger than where you do it.

I don't know if that helps anyone else. But it kept me sane (and even a degree hopeful) during my three-year "involuntary sabbatical" a few years back.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories