npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

glove-env-media

v1.0.0

Published

Video and audio stdlib adapter for glove-working-environment. Bridges ffmpeg into the agent's virtual filesystem as env:media — describe a video without looking at it, thumbnail, extract frames, clip, concat, transcode, extract and burn in subtitles, meas

Readme

glove-env-media

Video and audio stdlib adapter for glove-working-environment. Bridges ffmpeg into the agent's virtual filesystem as env:media — describe a video without looking at it, thumbnail, extract frames, clip, concat, transcode, extract and burn in subtitles, measure and normalise loudness, build a slideshow. Paths in, paths out.

pnpm add glove-env-media

Static ffmpeg and ffprobe binaries ship as dependencies, so there is no system requirement.

import { createWorkingEnvironment } from "glove-working-environment";
import { media } from "glove-env-media";

const env = await createWorkingEnvironment({ stdlib: [media()] });
// or point at a system build:
const env2 = await createWorkingEnvironment({ stdlib: [media({ ffmpegPath: "/usr/bin/ffmpeg", ffprobePath: "/usr/bin/ffprobe" })] });

ffmpeg runs on the host. The sandbox still spawns nothing.

Every other adapter is a pure library call, and "no process spawning" is one of the environment's stated non-goals — so this one deserves to be explicit rather than discovered.

The subprocess is started by this host-side adapter, never by sandboxed code. A script calls transcode(input, output) with two VFS paths. It cannot name a program, pass a flag, choose a codec string, or observe that a process was involved at all. That is the same trust position as any adapter — create(vfs) is the capability boundary and adapters are trusted host code — but it is a bigger deal here, so it is stated plainly.

Arguments always reach ffmpeg as an argv array through execFile, never as a shell string. A VFS path containing a space, a quote or a semicolon is just a filename; there is no shell to inject into. A test pins that.

What the model gets

| Function | Does | |---|---| | describe(path) | Codecs, duration, dimensions, fps, stream layout — no pixel data | | thumbnail(input, output, atSeconds?) | One frame, as an image | | frames(input, dir, { fps?, limit? }) | Frames into a directory as PNGs; returns the paths | | clip(input, output, { start, end }) | Cut a range, by seconds | | concat(inputs, output) | Join files end to end | | transcode(input, output, { crf?, scale?, fps? }) | Re-encode: container, quality, size, rate | | extractAudio(input, output, opts?) | Strip the audio out, at a codec/bitrate/sample rate you choose | | extractSubtitles(input, output, { track? }) | A subtitle stream as a file — and a format conversion on the way | | burnSubtitles(input, output, { track?, subtitles? }) | Subtitles painted into the picture, from a track or a file | | loudness(input) | Integrated LUFS, true peak, range — the units a platform spec is written in | | normalize(input, output, opts?) | Loudness to a target, in two passes; video copied untouched | | waveform(input, output, opts?) | The audio as a picture: dead air, clipping, is-anything-there | | slideshow(images, output, { frameMs?, size? }) | Stills → video |

import { describe, frames } from 'env:media';
import { contactSheet } from 'env:images';

/** A visual overview of a video nobody can watch. */
export default async function main() {
  const info = await describe('/inbox/recording.mp4');
  const shots = await frames('/inbox/recording.mp4', '/tmp/shots', { fps: 0.1, limit: 24 });
  await contactSheet(shots, '/out/overview.png', { cell: 240, columns: 6 });
  return info;
}

describe() is the first call on any media file. A video cannot go anywhere near a context window, so the summary and a frame or two are all a model has to reason with — which is exactly why "paths in, paths out" pays most here.

Details worth knowing

Media work is slow, so ffmpeg gets its own budget (two minutes per call by default, timeoutMs) rather than sharing the script timeout. A long transcode can still exceed it; the error says so and names the fix.

Arguments are checked before ffmpeg starts. An end before its start, a CRF of 99, a fractional pixel size, an empty slideshow — each fails as a sentence rather than as ffmpeg's usage output. When ffmpeg itself fails, only the last few lines of its stderr reach the model: the build banner is a hundred lines of nothing actionable.

Staging is cleaned up on both paths. Operations copy VFS bytes into a host temp directory, run, and read results back. The directory is removed in a finally — a failed transcode must not leave a gigabyte behind any more than a successful one, and there is a test that counts.

Slideshow stills are padded, not stretched, when size is given, so images of different shapes are not distorted to match each other.

normalize always measures first. loudnorm run blind is a dynamic compressor that lands somewhere near the target; the difference between "near −14 LUFS" and "−14 LUFS" is the entire reason anyone asks. So the first pass measures and the second applies that measurement as a fixed gain, and any video in the file is copied rather than re-encoded.

Filter graphs never carry a caller's path. A filename inside an ffmpeg filter graph is parsed: starts the next option, \ escapes — so there is no general safe escaping for one. burnSubtitles instead runs ffmpeg from inside the staging directory and names a bare file that was already reduced to [\w.-], and every colour or size that reaches a graph is validated against a pattern first.

Media files are claimed by the describe verb, so orientation on a mounted video needs no script.