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

@type9/sf6-frame-meter

v0.1.0

Published

Read the Street Fighter 6 replay viewer's on-screen frame meter out of a video and turn it into a semantic frame timeline.

Downloads

201

Readme

sf6-frame-meter

Read the Street Fighter 6 replay viewer's on-screen frame meter out of a video and turn it into a semantic frame timeline: per-player runs of startup / active / recovery / stun / armor / projectile / invincible frames, hitstop and freeze pauses, round boundaries and the score going into each round, all in video time.

Pure logic. The barrel has no Node or DOM dependency, so it is safe in server code, workers and client bundles alike. The two frame sources that bind to a runtime (ffmpeg via child_process, or a <video> element via OffscreenCanvas) ship as opt-in subpaths.

Status: 0.x. The reader is validated against 720p60 replay captures and their 1080p re-encodes; other layouts (Classic HUD variants, 4:3 captures, heavy overlays) are untested. Expect the document shape to be stable and the tracker heuristics to keep improving — FRAME_TIMELINE_VERSION tells you when a stored document is stale.

Install

pnpm add @type9/sf6-frame-meter
# Node processing without a system ffmpeg on PATH:
pnpm add ffmpeg-static

ffmpeg-static is an optional peer dependency — only the lib/ffmpeg-bin helper looks for it. Any ffmpeg ≥ 4 works if you pass its path yourself.

Usage

Node: video file → timeline document

import { buildFrameTimeline, MeterTracker } from "@type9/sf6-frame-meter"
import { getFfmpegBin } from "@type9/sf6-frame-meter/lib/ffmpeg-bin"
import {
	createFfmpegSource,
	probeVideo,
} from "@type9/sf6-frame-meter/lib/node-ffmpeg-source"

const ffmpegBin = getFfmpegBin() // or "ffmpeg" / "/usr/bin/ffmpeg"
const probe = await probeVideo("match.mp4", ffmpegBin)

const source = createFfmpegSource({
	input: "match.mp4",
	ffmpegBin,
	width: probe.width,
	height: probe.height,
	// optional: startSec, durationSec, calibrationWindow
})

const tracker = new MeterTracker()
for await (const sample of source.frames) tracker.feed(sample)
const result = tracker.finish()

const geo = source.finalGeometry()
const doc = buildFrameTimeline(result, {
	fps: probe.fps,
	width: probe.width,
	height: probe.height,
	geometry: {
		cellPitch: geo.sepX[1]! - geo.sepX[0]!,
		firstCellX: geo.cellX[0]!,
		rowY: geo.rowY,
		calibrated: geo.calibrated,
	},
})

ffmpeg does the cropping, so the pipe carries only the meter rows, the HUD band with the round-win squares and a small thumbnail for duplicate-frame detection (~0.6 MB per 720p frame instead of 2.7 MB). A 10-minute 720p60 VOD processes in well under real time on a laptop.

Browser: <video> element → tracker

import { buildFrameTimeline } from "@type9/sf6-frame-meter"
import { trackVideoElement } from "@type9/sf6-frame-meter/lib/browser-source"

const tracker = trackVideoElement(videoEl, {
	onDone: (result) => {
		/* buildFrameTimeline(result, …) as above */
	},
})
videoEl.play()

Driven by requestVideoFrameCallback; keep playbackRate <= 1 so no presented frame is skipped. For guaranteed every-frame access, decode with WebCodecs and call tracker.feed(sampleFromReader(...)) per VideoFrame instead.

The document

interface FrameTimelineDocument {
	version: number // FRAME_TIMELINE_VERSION it was built with
	source: { width; height; fps; videoFrames; durationMs }
	geometry: { cellPitch; firstCellX; rowY: [p1, p2]; calibrated }
	rounds: RoundRange[] // index, number, score going in, start/dur
	lanes: { p1: LaneSpan[]; p2: LaneSpan[] } // cls + startMs + durMs
	pauses: PauseSpan[] // kind: "hitstop" | "freeze" | "ko"
	hidden: TimeRange[] // meter off screen (between rounds)
	stats: { meterFrames; dupFrames; pausedFrames; anomalies }
}

All times are video milliseconds. Lanes are semantic (CellClass plus a span); palette, lane heights, markers and derived readouts are display concerns — CLASS_COLOURS exports the measured on-screen colours if you want to match the game.

How the meter is read

  • The meter draws 80 cells per row (P1 above P2), one cell per game frame, left to right, wrapping at cell 79 with a dimmed ghost of the previous pass left behind. Captures drop and duplicate frames (~10% on the reference VOD), so the tracker counts cells, not video frames and resolves unseen stretches by where the cursor resumes.
  • Cells are classified by hue (smallest channel removed, unit sum) against measured fresh medians, with a brightness-ratio band to tell live paint from the ghost pass. Run-length digit glyphs over the last cells of a run are folded back into the run.
  • Hitstop, super freezes and both-players-neutral stops are not drawn; the tracker tells a freeze (a player is in a state they cannot be idle in, or an action continues across the gap) from a stop (both neutral) by what was on the bar — a freeze holds the lane colour, a stop goes dark.
  • Rounds are the visible stretches of the meter; the win badges beside the timer give the score going into each round and, when the winner's badge lights before the meter hides, the KO frame.
  • Pixel geometry is expressed for a 1280x720 reference and scaled to the source size; calibrateGeometry() refines it from a frame where the bar has colour (the Node source does this automatically over the first calibrationWindow frames).

Data-source notes

The reader needs no external frame data — it reads what the game draws. If you want to join the timeline to move names or canonical frame data, these are the sources we know of:

  • sfstats.app has a public replay API, but it sits behind a Cloudflare browser challenge that rejects non-browser clients (curl, fetch, headless browsers), so it is not usable as an automated data source.
  • MMDK (alphazolam/MMDK, REFramework mod) ships per-character dumps of the game's own action tables (Names.json, moves_dict, HIT_DT): action id → name, startup/active/recovery windows, cancel windows, hitstun/hitstop per outcome. This is game-truth, dumped from the running game.
  • sf6-remake (colefoster/sf6-remake) is a headless frame-data engine whose ADRs document the frame identities (blockstun, counter and punish-counter bonuses, Drive Rush cancel freeze) and bundles a FAT frame-data export.

Treat community frame-data sites as derived; the dumps above are the reference when numbers disagree.

Regression check

There is no test runner yet. Re-run a reference VOD and compare the run labels the game prints in the bar against the lanes at the same timestamp:

pnpm run run -- --input match.mp4 --out doc.json
pnpm exec tsx src/scripts/debug-states.ts --input match.mp4 --from 11.8 --to 13.2

Meter-frame count, the pause histogram (hitstop clusters at 9–13 frames) and the anomaly count are the numbers to watch.

License

MIT © Lossless. Street Fighter and its frame meter are the property of Capcom; this project reads a video of the game's UI and is not affiliated with or endorsed by Capcom.