Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

## Project Overview

`@plutotcool/fsv` is a TypeScript library that defines and implements the **FSV (Fast Scrubbing Video)** format — a custom binary video format optimized for frame-accurate scrubbing on the web, with optional alpha-channel support.
`@plutotcool/fsv` is a TypeScript library that defines and implements the **FSV (Fast Scrubbing Video)** format — a video container format optimized for frame-accurate scrubbing on the web, using H.264 and H.265 codecs with optional alpha-channel support.

The library ships two distinct APIs:

| API | Environment | Purpose |
|-----|-------------|---------|
| **Conversion** | Node.js | Converts any video file to `.fsv` using ffmpeg (`node-av`) for encoding and config extraction |
| **Conversion** | Node.js | Converts any video file to `.fsv` (H.264/H.265 container) using ffmpeg (`node-av`) for encoding and config extraction |
| **Decoding / Rendering** | Browser | Loads, demuxes, decodes, and WebGL2-renders `.fsv` files using native browser WebCodecs and WebGL2 APIs |

The package is published to the GitHub Packages registry (`https://npm.pkg.github.com`) under the `@plutotcool` scope.
Expand Down Expand Up @@ -185,8 +185,8 @@ The manifest is serialized as a compact flat number array: each frame is 4 conse
- Uses `node-av` (ffmpeg bindings) for decoding and encoding
- Uses a `FilterComplexAPI` for pixel format conversion to `yuv420p`
- For alpha videos, uses ffmpeg's `alphaextract` filter to produce separate color + alpha streams
- Supported output codecs: `libx264`, `libx265`, `libvpx-vp8`, `libvpx-vp9`
- Default output codec: `libx264` → mp4 container
- Supported output codecs: `libx264` (H.264), `libx265` (H.265)
- Default output codec: `libx264`
- Default encoder settings: `crf=20`, `gopSize=5`, `maxBFrames=0`, `threadType=FF_THREAD_FRAME`
- Sets `AV_CODEC_FLAG_GLOBAL_HEADER` on each encoder so SPS/PPS go into extradata (not inlined in keyframes)
- Collects raw packets in memory as `Packet[]` — no temp files or intermediate container
Expand All @@ -200,7 +200,7 @@ The manifest is serialized as a compact flat number array: each frame is 4 conse
### Muxer (Node.js only — `src/core/Muxer.ts`)

- Accepts `Packet[]` arrays from Converter (no container demuxing)
- Converts Annex B → AVCC bitstream for H.264/H.265 packets (VP8/VP9 pass through unchanged)
- Converts Annex B → AVCC bitstream for H.264/H.265 packets
- Packs packets and manifest directly into FSV binary format
- Reads width/height from `config.codedWidth`/`config.codedHeight` for manifest serialization

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@
"import": "./dist/cli/main.mjs",
"require": "./dist/cli/main.cjs"
},
"./core/Codec": {
"import": "./dist/core/Codec.mjs",
"require": "./dist/core/Codec.cjs"
},
"./core/Converter": {
"import": "./dist/core/Converter.mjs",
"require": "./dist/core/Converter.cjs"
Expand Down
9 changes: 7 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ FSV (short for "Fast Scrubbing Video") is a video format optimized for fast
scrubbing on the web, supporting alpha channel. It has been inspired by the
[ActiveFrame](https://github.com/activetheory/activeframe) project.

More specifically, FSV is a container for h264 and h265. It is designed to be
easily demuxable on the browser, and contains additional metadata to allow for
frame-accurate scrubbing and efficient seeking. See the [format](#format)
section for more details.

[See the demo](https://plutotcool-fsv.vercel.app)

<img src="demo.gif" width="320" height="307"/>
Expand Down Expand Up @@ -78,8 +83,8 @@ await Converter.convert('input.webm', 'output.fsv', {

The input file codec is automatically detected if not provided. Yet the
conversion may fail (especially for alpha videos) if the input codec is not
compatible with the conversion process. For VP8/VP9 videos, you may need to
explicitly set the input codec to `libvpx-vp8` or `libvpx-vp9`.
compatible with the conversion process. For VP8/VP9 input videos, you may
need to explicitly set the input codec to `libvpx-vp8` or `libvpx-vp9`.

## Rendering

Expand Down
7 changes: 4 additions & 3 deletions src/cli/convert.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineCommand } from 'citty'
import { Converter, FF_ENCODER_LIBX264, type ConvertOptions } from '~/core/Converter'
import { Converter, type ConvertOptions } from '~/core/Converter'
import { resolveCodec } from '~/core/Codec'
import { Logger } from '~/core/Logger'

export default defineCommand({
Expand All @@ -26,7 +27,7 @@ export default defineCommand({
},
'output-codec': {
type: 'string',
default: FF_ENCODER_LIBX264,
default: 'libx264',
valueHint: 'codec',
description: 'Codec to use for encoding the video tracks.'
},
Expand Down Expand Up @@ -60,7 +61,7 @@ export default defineCommand({
}),

alpha: args.alpha,
outputCodec: args['output-codec'],
outputCodec: resolveCodec(args['output-codec']),
inputCodec: args['input-codec'] === 'auto'
? undefined
: args['input-codec'] as ConvertOptions['inputCodec'],
Expand Down
38 changes: 38 additions & 0 deletions src/core/Codec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
FF_ENCODER_LIBX264 as H264,
FF_ENCODER_LIBX265 as H265
} from 'node-av/constants'

export const CODECS = [
H264,
H265
]

export const DEFAULT_CODEC = H264

export {
H264,
H265
}

export type Codec = typeof CODECS[number]

export function isCodec(codec: unknown): codec is Codec {
return CODECS.includes(codec as Codec)
}

export function assertCodec(codec: unknown): asserts codec is Codec {
if (!isCodec(codec)) {
throw new Error(`Unsupported codec: ${codec}. Supported codecs: ${CODECS}`)
}
}

export function resolveCodec(codec: unknown): Codec {
if (!codec) {
return DEFAULT_CODEC
}

assertCodec(codec)

return codec
}
Loading
Loading