zvidlib 0.5.0

Frame-accurate video and audio I/O for native and WebAssembly applications
Documentation
import init, { AudioBuffer, CreateOptions, MediaOutput, SampleRange, VideoFrame, audioEncodeSupport, videoEncodeSupport } from "./pkg/zvidlib.js";

const encodeButton = document.querySelector("#encode");
const download = document.querySelector("#download");
const preview = document.querySelector("#preview");
const status = document.querySelector("#status");
const width = 320, height = 180, frameRate = 30, sampleRate = 48_000;

function pixelsForFrame(index) {
  const pixels = new Uint8Array(width * height * 4);
  for (let y = 0; y < height; y += 1) for (let x = 0; x < width; x += 1) {
    const offset = (y * width + x) * 4;
    pixels[offset] = (x + index * 7) & 255;
    pixels[offset + 1] = (y * 2 + index * 3) & 255;
    pixels[offset + 2] = (x + y + index * 11) & 255;
    pixels[offset + 3] = 255;
  }
  return pixels;
}

function tone(start, frames) {
  const samples = new Float32Array(frames * 2);
  for (let frame = 0; frame < frames; frame += 1) {
    const value = Math.sin(2 * Math.PI * 440 * (start + frame) / sampleRate) * 0.15;
    samples[frame * 2] = value; samples[frame * 2 + 1] = value;
  }
  return samples;
}

await init();
const videoAvailable = videoEncodeSupport();
const audioAvailable = audioEncodeSupport();
status.textContent = videoAvailable ? `Video encoding is available. AAC-LC audio: ${audioAvailable ? "available" : "unavailable"}.` : "This browser does not expose the WebCodecs AV1 encoder required by this example.";
encodeButton.disabled = !videoAvailable;

encodeButton.addEventListener("click", async () => {
  encodeButton.disabled = true; download.hidden = true;
  try {
    const options = new CreateOptions("mp4"); options.setTimeline(frameRate, 1, sampleRate);
    const output = await MediaOutput.create(options);
    const video = output.video(0), audio = audioAvailable ? output.audio(0) : null;
    const samplesPerFrame = sampleRate / frameRate;
    for (let index = 0; index < frameRate; index += 1) {
      await video.put(BigInt(index), VideoFrame.rgba(width, height, pixelsForFrame(index)));
      if (audio) {
        const start = index * samplesPerFrame;
        const range = new SampleRange(BigInt(start), BigInt(start + samplesPerFrame));
        await audio.put(BigInt(index), new AudioBuffer(range, sampleRate, 2, tone(start, samplesPerFrame)));
      }
      status.textContent = `Encoding frame ${index + 1} of ${frameRate}…`;
    }
    const blob = await output.finish(), url = URL.createObjectURL(blob);
    if (preview.src) URL.revokeObjectURL(preview.src);
    preview.src = url; download.href = url; download.hidden = false;
    status.textContent = `Created ${blob.size.toLocaleString()} bytes of MP4${audio ? " with synchronized AAC" : " (video only)"}.`;
  } catch (error) {
    status.textContent = `Encoding failed: ${error.message || error}`;
  } finally { encodeButton.disabled = false; }
});