{"_id":"@altqx/rwss","_rev":"5-aa1f6ac32132055c1e0bcdf9c64de859","name":"@altqx/rwss","dist-tags":{"latest":"0.1.4"},"versions":{"0.1.0":{"name":"@altqx/rwss","version":"0.1.0","keywords":["rwss","rassa","ass","ssa","subtitle","wasm","rust","rendering","video"],"author":{"name":"altqx"},"license":"MIT","_id":"@altqx/rwss@0.1.0","maintainers":[{"name":"altinat","email":"poiiiii4yy@gmail.com"}],"homepage":"https://github.com/altqx/rwss#readme","bugs":{"url":"https://github.com/altqx/rwss/issues"},"dist":{"shasum":"46268c2bd551388c60e9101efda08df2f768b784","tarball":"https://registry.npmjs.org/@altqx/rwss/-/rwss-0.1.0.tgz","fileCount":43,"integrity":"sha512-qibYmys1MXmDlbZIDeuzSWSnazlc6UJ4fMT9vc2vqNmlqYgMEK9meC7I81IyzR614uoyYbwlgL6rlJ4La0hktQ==","signatures":[{"sig":"MEUCIQCVeVsUwxB9pQvnbB3GoYlf++B4O82Wg0iqO5IQ1hpLvwIgAMhtlE50pYS7K5PyMvRKIoMtSFaALSJ4iWQj2v+2Vdc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1878641},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./pkg":{"types":"./pkg/rwss.d.ts","import":"./pkg/rwss.js"}},"gitHead":"9f25d5aeffeda02c2e035aefc97b661fdb364ebd","scripts":{"test":"bun run test:rust && bun run typecheck && bun run test:ts","build":"bun run build:wasm:release && bun run build:ts","clean":"rm -rf pkg pkg-bundler dist target tests/artifacts","format":"cargo fmt","test:ts":"bun test tests/*.test.ts","build:ts":"bun x tsc -p tsconfig.json && bun run build:assets","test:e2e":"bun test tests/e2e-render.test.ts","test:rust":"cargo test","typecheck":"bun x tsc -p tsconfig.json --pretty false && bun x tsc -p tsconfig.test.json --noEmit --pretty false","build:wasm":"wasm-pack build --target web --out-dir pkg","build:assets":"mkdir -p dist && cp src/default.woff2 dist/default.woff2","build:wasm:bundler":"wasm-pack build --target bundler --out-dir pkg-bundler","build:wasm:release":"wasm-pack build --release --target web --out-dir pkg"},"_npmUser":{"name":"altinat","email":"poiiiii4yy@gmail.com"},"repository":{"url":"git+https://github.com/altqx/rwss.git","type":"git"},"_npmVersion":"12.0.2","description":"Web browser ASS/SSA renderer wrapper around rassa","directories":{},"_nodeVersion":"24.19.0","dependencies":{"@webgpu/types":"^0.1.71"},"_hasShrinkwrap":false,"devDependencies":{"@types/bun":"^1.3.14","typescript":"^7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/rwss_0.1.0_1786618553220_0.502818795520527","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@altqx/rwss","version":"0.1.1","keywords":["rwss","rassa","ass","ssa","subtitle","wasm","rust","rendering","video"],"author":{"name":"altqx"},"license":"MIT","_id":"@altqx/rwss@0.1.1","maintainers":[{"name":"altinat","email":"poiiiii4yy@gmail.com"}],"homepage":"https://github.com/altqx/rwss#readme","bugs":{"url":"https://github.com/altqx/rwss/issues"},"dist":{"shasum":"59cc69b62c664ecf9ce63b105c5ced5af1cd746f","tarball":"https://registry.npmjs.org/@altqx/rwss/-/rwss-0.1.1.tgz","fileCount":43,"integrity":"sha512-FgYoCiwT6GhdtcBwDle+rJuLqyC4Pu6k3HXZtYsEjB8jjjplP5bUxLDjAyGi97er0JfOWClVXYzPfYEfQNu0ag==","signatures":[{"sig":"MEYCIQDdYfax7MyXy29qlsEhD0+nqojJDXhqjcay9SZ215dQtAIhALl3qPnjZ4uxos2lqmrD/2qspg3CI4hZLzkUHnUPK+Y8","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@altqx%2frwss@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1899645},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./pkg":{"types":"./pkg/rwss.d.ts","import":"./pkg/rwss.js"}},"gitHead":"62e6c3b0650b4cf41bd37c53b1719fb1cdc96ab2","scripts":{"test":"bun run test:rust && bun run typecheck && bun run test:ts","build":"bun run build:wasm:release && bun run build:ts","clean":"rm -rf pkg pkg-bundler dist target tests/artifacts","format":"cargo fmt","test:ts":"bun test tests/*.test.ts","build:ts":"bun x tsc -p tsconfig.json && bun run build:assets","test:e2e":"bun test tests/e2e-render.test.ts","test:rust":"cargo test","typecheck":"bun x tsc -p tsconfig.json --pretty false && bun x tsc -p tsconfig.test.json --noEmit --pretty false","build:wasm":"wasm-pack build --target web --out-dir pkg","build:assets":"mkdir -p dist && cp src/default.woff2 dist/default.woff2","build:wasm:bundler":"wasm-pack build --target bundler --out-dir pkg-bundler","build:wasm:release":"wasm-pack build --release --target web --out-dir pkg"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:dbe9fbe5-14d5-4c23-8664-afc6432de095"}},"repository":{"url":"git+https://github.com/altqx/rwss.git","type":"git"},"_npmVersion":"12.0.2","description":"Web browser ASS/SSA renderer wrapper around rassa","directories":{},"_nodeVersion":"24.18.0","dependencies":{"@webgpu/types":"^0.1.71"},"_hasShrinkwrap":false,"devDependencies":{"@types/bun":"^1.3.14","typescript":"^7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/rwss_0.1.1_1786619629185_0.7118866172455891","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@altqx/rwss","version":"0.1.2","keywords":["rwss","rassa","ass","ssa","subtitle","wasm","rust","rendering","video"],"author":{"name":"altqx"},"license":"MIT","_id":"@altqx/rwss@0.1.2","maintainers":[{"name":"altinat","email":"poiiiii4yy@gmail.com"}],"homepage":"https://github.com/altqx/rwss#readme","bugs":{"url":"https://github.com/altqx/rwss/issues"},"dist":{"shasum":"e1c68098894453a1f062747295f05382657e6790","tarball":"https://registry.npmjs.org/@altqx/rwss/-/rwss-0.1.2.tgz","fileCount":43,"integrity":"sha512-+qF+rnWVyOkX4F4CK6NPJlXexBdGLzzKwySSilxL3AdpvdBHF1oSu49RBjmfmYNjCkTNUEfuTLw9Mf4pU3nY0Q==","signatures":[{"sig":"MEYCIQC4MpsEayBJPodbLnFzvM3QByj6u8x41c3/RLKbW2RKsAIhALjm39S1DaZn/vw6FPApIFBBapS5WZKoDZaDOOq9/3WL","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@altqx%2frwss@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1903467},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js","require":"./dist/index.js"},"./pkg":{"types":"./pkg/rwss.d.ts","import":"./pkg/rwss.js","default":"./pkg/rwss.js"}},"gitHead":"e7164069c907819b614003202a8409618ffc6f29","scripts":{"test":"bun run test:rust && bun run typecheck && bun run test:ts","build":"bun run build:wasm:release && bun run build:ts","clean":"rm -rf pkg pkg-bundler dist target tests/artifacts","format":"cargo fmt","test:ts":"bun test tests/*.test.ts","build:ts":"bun x tsc -p tsconfig.json && bun run build:assets","test:e2e":"bun test tests/e2e-render.test.ts","test:rust":"cargo test","typecheck":"bun x tsc -p tsconfig.json --pretty false && bun x tsc -p tsconfig.test.json --noEmit --pretty false","build:wasm":"wasm-pack build --target web --out-dir pkg","build:assets":"mkdir -p dist && cp src/default.woff2 dist/default.woff2","build:wasm:bundler":"wasm-pack build --target bundler --out-dir pkg-bundler","build:wasm:release":"wasm-pack build --release --target web --out-dir pkg"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:dbe9fbe5-14d5-4c23-8664-afc6432de095"}},"repository":{"url":"git+https://github.com/altqx/rwss.git","type":"git"},"_npmVersion":"12.0.2","description":"Web browser ASS/SSA renderer wrapper around rassa","directories":{},"_nodeVersion":"24.18.0","dependencies":{"@webgpu/types":"^0.1.71"},"_hasShrinkwrap":false,"devDependencies":{"@types/bun":"^1.3.14","typescript":"^7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/rwss_0.1.2_1786633109640_0.5952257636839231","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@altqx/rwss","version":"0.1.3","keywords":["rwss","rassa","ass","ssa","subtitle","wasm","rust","rendering","video"],"author":{"name":"altqx"},"license":"MIT","_id":"@altqx/rwss@0.1.3","maintainers":[{"name":"altinat","email":"poiiiii4yy@gmail.com"}],"homepage":"https://github.com/altqx/rwss#readme","bugs":{"url":"https://github.com/altqx/rwss/issues"},"dist":{"shasum":"972f1b8d64ce1462a7c50751641ee05e31975a91","tarball":"https://registry.npmjs.org/@altqx/rwss/-/rwss-0.1.3.tgz","fileCount":43,"integrity":"sha512-1rDXZbODhEVem3u4W/ElJRo7MFd2U6EhU4GkCffk/haazb09JCKZv/EMFZhmEcdGj+dwM9BsP2U++4veBUt+mw==","signatures":[{"sig":"MEYCIQDpSbdRl1PxCsDoLQrdQymGlIUTTMIsyuoCCKnxjjMrPQIhAJbYHLK6UWzCuA5DAazWc9Kdn5L1mOf0FXwuo09NDVhv","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@altqx%2frwss@0.1.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":1905009},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js","require":"./dist/index.js"},"./pkg":{"types":"./pkg/rwss.d.ts","import":"./pkg/rwss.js","default":"./pkg/rwss.js"}},"gitHead":"c003cc0b72b1cc5f4a002554ab9691d328f48665","scripts":{"test":"bun run test:rust && bun run typecheck && bun run test:ts","build":"bun run build:wasm:release && bun run build:ts","clean":"rm -rf pkg pkg-bundler dist target tests/artifacts","format":"cargo fmt","test:ts":"bun test tests/*.test.ts","build:ts":"bun x tsc -p tsconfig.json && bun run build:assets","test:e2e":"bun test tests/e2e-render.test.ts","test:rust":"cargo test","typecheck":"bun x tsc -p tsconfig.json --pretty false && bun x tsc -p tsconfig.test.json --noEmit --pretty false","build:wasm":"wasm-pack build --target web --out-dir pkg","build:assets":"mkdir -p dist && cp src/default.woff2 dist/default.woff2","build:wasm:bundler":"wasm-pack build --target bundler --out-dir pkg-bundler","build:wasm:release":"wasm-pack build --release --target web --out-dir pkg"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:dbe9fbe5-14d5-4c23-8664-afc6432de095"}},"repository":{"url":"git+https://github.com/altqx/rwss.git","type":"git"},"_npmVersion":"12.0.2","description":"Web browser ASS/SSA renderer wrapper around rassa","directories":{},"_nodeVersion":"24.19.0","dependencies":{"@webgpu/types":"^0.1.71"},"_hasShrinkwrap":false,"devDependencies":{"@types/bun":"^1.3.14","typescript":"^7.0.2"},"_npmOperationalInternal":{"tmp":"tmp/rwss_0.1.3_1786667799416_0.8366303267610697","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@altqx/rwss","version":"0.1.4","author":{"name":"altqx"},"license":"MIT","description":"Web browser ASS/SSA renderer wrapper around rassa","repository":{"type":"git","url":"git+https://github.com/altqx/rwss.git"},"keywords":["rwss","rassa","ass","ssa","subtitle","wasm","rust","rendering","video"],"main":"dist/index.js","module":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js","default":"./dist/index.js"},"./pkg":{"types":"./pkg/rwss.d.ts","import":"./pkg/rwss.js","default":"./pkg/rwss.js"}},"scripts":{"build:wasm":"wasm-pack build --target web --out-dir pkg","build:wasm:release":"wasm-pack build --release --target web --out-dir pkg","build:wasm:bundler":"wasm-pack build --target bundler --out-dir pkg-bundler","build:assets":"mkdir -p dist && cp src/default.woff2 dist/default.woff2","build:ts":"bun x tsc -p tsconfig.json && bun run build:assets","build":"bun run build:wasm:release && bun run build:ts","typecheck":"bun x tsc -p tsconfig.json --pretty false && bun x tsc -p tsconfig.test.json --noEmit --pretty false","test:rust":"cargo test","test:ts":"bun test tests/*.test.ts","test:e2e":"bun test tests/e2e-render.test.ts","test":"bun run test:rust && bun run typecheck && bun run test:ts","clean":"rm -rf pkg pkg-bundler dist target tests/artifacts","format":"cargo fmt"},"devDependencies":{"@types/bun":"^1.3.14","typescript":"^7.0.2"},"engines":{"node":">=18"},"dependencies":{"@webgpu/types":"^0.1.71"},"gitHead":"d35cf648459a14dc6207cd1f1aa4289d90e0f466","_id":"@altqx/rwss@0.1.4","bugs":{"url":"https://github.com/altqx/rwss/issues"},"homepage":"https://github.com/altqx/rwss#readme","_nodeVersion":"24.19.0","_npmVersion":"12.0.2","dist":{"integrity":"sha512-ndBQUoc9bTXxNxBiG2sWWPPpNvSyOjVQHX4d8iGdlHhgI8ZvjACjGYfcgcPUPbbhhDkOljL6dBmIiSpTCghmoQ==","shasum":"9a5dc4ce43fb8dc9258481403f1299fa60b83dc2","tarball":"https://registry.npmjs.org/@altqx/rwss/-/rwss-0.1.4.tgz","fileCount":43,"unpackedSize":2553240,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@altqx%2frwss@0.1.4","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIDBzoc2kaFgKdSM3NNATMhMR4EL86INhmgN4Rx3JlIPAAiB4GKtMJUeMKY1lBgvp/jB3Ngjxg6GBbEMYf073V7oy0A=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:dbe9fbe5-14d5-4c23-8664-afc6432de095"}},"directories":{},"maintainers":[{"name":"altinat","email":"poiiiii4yy@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/rwss_0.1.4_1787209080883_0.7810086190495278"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-13T10:55:53.009Z","modified":"2026-08-20T06:58:01.360Z","0.1.0":"2026-08-13T10:55:53.412Z","0.1.1":"2026-08-13T11:13:49.355Z","0.1.2":"2026-08-13T14:58:29.856Z","0.1.3":"2026-08-14T00:36:39.618Z","0.1.4":"2026-08-20T06:58:01.046Z"},"bugs":{"url":"https://github.com/altqx/rwss/issues"},"author":{"name":"altqx"},"license":"MIT","homepage":"https://github.com/altqx/rwss#readme","keywords":["rwss","rassa","ass","ssa","subtitle","wasm","rust","rendering","video"],"repository":{"type":"git","url":"git+https://github.com/altqx/rwss.git"},"description":"Web browser ASS/SSA renderer wrapper around rassa","maintainers":[{"name":"altinat","email":"poiiiii4yy@gmail.com"}],"readme":"# rwss\n\nHigh-performance browser ASS/SSA subtitle renderer powered by the pure-Rust [`rassa`](https://github.com/altqx/rassa) renderer and exported through WebAssembly.\n\n`rwss` follows the same browser package shape as [`libbitsub`](https://github.com/altqx/libbitsub): a Rust core, a generated `wasm-bindgen` package under `pkg/`, TypeScript wrappers under `src/ts/`, and public barrel exports from `src/index.ts` / `src/wrapper.ts`. It is intentionally modern-browser-first: WASM is the only subtitle engine path, the high-level video renderer uses `requestVideoFrameCallback`, and GPU helpers target WebGPU/WebGL2 with deterministic Canvas2D/CPU fallback where needed.\n\n## Features\n\n- ASS/SSA parsing and rendering through `rassa` + WASM\n- Automatic WASM URL resolution and browser pre-init, matching the libbitsub package pattern\n- High-level video/canvas renderer (`AssRenderer`, alias `AkariSub`) with overlay canvas creation, playback sync, resize handling, and render events\n- Modern browser scheduling with `HTMLVideoElement.requestVideoFrameCallback` for video-backed render loops\n- ASS metadata, events, styles, timestamp list, and parser introspection APIs\n- Runtime track editing helpers for events/styles, style override, default font updates, and font injection\n- Browser font loading for TTF/OTF/TTC/OTC/WOFF/WOFF2 URLs or bytes, plus optional `queryLocalFonts`\n- Frame export helpers for `ImageData`, `ImageBitmap`, `Blob`, and canvas targets, including direct bounds-cropped WASM output\n- WebGPU and WebGL2 image-composition surfaces with direct no-readback presentation and CPU fallback for unsupported/headless runtimes\n- Exact-frame `requestVideoFrameCallback` sampling with encoded `frameTimeline` maps, HLS/Shaka clock-domain detection, and integer-millisecond rassa timestamps\n- Exact-frame prefetch runway (`framePrefetch`, default 2, max 24) that can defer `ready` / `trackReady` until the current frame and runway are prepared\n- Adaptive presentation-latency compensation, compositor refresh-grid reanchoring, and immediate prepared-frame commit on RVFC arrival\n- Static-cue reuse across video frames, while animated, karaoke, move, fade, and effect events remain time-sampled\n- Custom canvases stay on the main thread by default; worker/raw ASS WebGL2 composition is an explicit opt-in\n- Safety limits for fonts (32 MiB) and composed image planes (8192 images / 32M pixels)\n- Worker/offscreen client/runtime protocol for OffscreenCanvas rendering handoff, including prepare/present/frame-timeline messages\n- AES-GCM encrypted subtitle content transport with raw-key helpers for Akari-style handoff\n- TypeScript support with exported renderer, worker, metadata, event, style, timing, and frame types\n\n## Installation\n\n```bash\nnpm install @altqx/rwss\n# or\nbun add @altqx/rwss\n```\n\n```bash\ndeno add jsr:@altq/rwss\n```\n\n```bash\ncargo add rwss\n```\n\n## WASM setup\n\nIn most bundler-based projects, no manual WASM setup is required. `rwss` resolves the generated WASM asset relative to the package module URL, so bundlers such as Vite, webpack, and Rollup can emit the asset automatically.\n\nThe WASM module initializes automatically, matching libbitsub:\n\n- high-level renderers call `initWasm()` through parser loading\n- `initWasm()` imports the generated glue (`pkg/rwss.js`) and lets wasm-bindgen resolve `rwss_bg.wasm` next to that file\n- workers receive both `wasmUrl` and `glueUrl` and dynamically import the glue\n- importing the library triggers a non-blocking browser pre-init\n- repeated `initWasm()` calls are safe and deduplicated\n\nIf your app serves package files in a way that does not expose the emitted WASM asset to the browser, copy both generated files and pass `wasmUrl`, or serve the package assets directly:\n\n```bash\nmkdir -p public/rwss\ncp node_modules/@altqx/rwss/pkg/rwss_bg.wasm node_modules/@altqx/rwss/pkg/rwss.js public/rwss/\n```\n\n```ts\nimport { initWasm } from '@altqx/rwss'\n\nawait initWasm('/rwss/rwss_bg.wasm')\n```\n\n## Building from source\n\nPrerequisites:\n\n- Rust\n- wasm-pack\n- Bun\n\n```bash\ncargo install wasm-pack\nbun run build\n```\n\nUseful development commands:\n\n```bash\nPATH=\"$HOME/.bun/bin:$PATH\" bun run build:wasm\nPATH=\"$HOME/.bun/bin:$PATH\" bun run build:ts\nPATH=\"$HOME/.bun/bin:$PATH\" bun run test\n```\n\n## Quick start\n\nThe high-level renderer manages subtitle loading, canvas overlay creation, playback sync, browser font setup, and render timing:\n\n```ts\nimport { AssRenderer } from '@altqx/rwss'\n\nconst renderer = new AssRenderer({\n  video: videoElement,\n  subUrl: '/subtitles/movie.ass',\n  debug: true,\n  onEvent: (event) => {\n    if (event.type === 'render') {\n      console.log('rendered frame', event.time, event.compositionCount, event.renderTime)\n    }\n  },\n  onError: console.error\n})\n\n// later\nrenderer.destroy()\n```\n\nIf you already have subtitle text or bytes, pass `subContent` instead of `subUrl`:\n\n```ts\nimport { AssRenderer } from '@altqx/rwss'\n\nconst renderer = new AssRenderer({\n  video: videoElement,\n  subContent: assText\n})\n```\n\n`AkariSub` is exported as an alias of `AssRenderer` for integrations that use an AkariSub-like entry point:\n\n```ts\nimport { AkariSub } from '@altqx/rwss'\n\nconst renderer = new AkariSub({ video: videoElement, subUrl: '/subtitles/movie.ass' })\n```\n\n### Exact-frame timing\n\n`rwss` sends rassa an integer millisecond timestamp. Fractional media times are floored after snapping only floating-point roundoff, matching libass's `Start <= now < Start + Duration` event boundaries. `timeOffset` and `renderAhead` are measured in seconds.\n\nFor frame-locked VOD playback, provide the encoded video's presentation timestamps. rwss prepares a small window of full subtitle frames and commits the matching frame inside `requestVideoFrameCallback`:\n\n```ts\nconst frameTimeline = Object.assign(new Float64Array([0, 0.041708, 0.083417]), {\n  mediaTimeOrigin: 0,\n  subtitleTimeOffset: 0\n})\n\nconst renderer = new AssRenderer({\n  video: videoElement,\n  subContent,\n  frameTimeline,\n  framePrefetch: 2,\n  adaptiveTiming: true\n})\n\nrenderer.setFrameTimeline(frameTimeline)\nrenderer.setFrameTimeline(null) // return to continuous-time rendering\n```\n\nCustom canvases stay on the main thread by default. Set both `offscreenRender: true` and `rawAssImageGpu: true` to opt into worker/raw-plane WebGL2 composition when dense overlays benefit from it.\n\n## Low-level parser API\n\nUse `openAss()` when you want in-memory parsing/rendering without a video element:\n\n```ts\nimport { openAss } from '@altqx/rwss'\n\nconst subtitles = await openAss(assText)\n\nconsole.log(subtitles.format)\nconsole.log(subtitles.metadata)\nconsole.log(subtitles.timestamps)\nconsole.log(subtitles.getEvents())\nconsole.log(subtitles.getStyles())\n\nconst frame = subtitles.renderAtTimestamp(120.5)\nconst rendered = subtitles.renderFrameDataAtTimestamp(120.5, { crop: 'bounds' })\n\nsubtitles.dispose()\n```\n\n`openAss()` initializes WASM for you and returns an `OpenedAssSubtitles` handle with:\n\n- `metadata`\n- `timestamps`\n- `renderAtIndex(index)`\n- `renderAtTimestamp(timeSeconds)`\n- `renderFrameDataAtIndex(index, options?)`\n- `renderFrameDataAtTimestamp(timeSeconds, options?)`\n- `getEvents()`\n- `getStyles()`\n- `clearCache()`\n- `dispose()`\n\n## Frame export helpers\n\nParser output can be flattened into exportable RGBA pixels for previews, editors, snapshots, or visual diffing:\n\n```ts\nimport { openAss, renderFrameData, toBlob, toCanvas, toImageBitmap } from '@altqx/rwss'\n\nconst subtitles = await openAss(assText)\nconst subtitleFrame = subtitles.renderAtTimestamp(120.5)\nconst rendered = subtitleFrame ? renderFrameData(subtitleFrame, { crop: 'bounds' }) : undefined\n\nif (rendered) {\n  const canvas = toCanvas(rendered)\n  const bitmap = await toImageBitmap(rendered)\n  const pngBlob = await toBlob(rendered)\n\n  console.log({\n    width: rendered.imageData.width,\n    height: rendered.imageData.height,\n    offsetX: rendered.offsetX,\n    offsetY: rendered.offsetY,\n    bounds: rendered.bounds,\n    canvas,\n    bitmap,\n    pngBlob\n  })\n}\n```\n\nCropping modes:\n\n- `bounds` uses a direct WASM compositor, returns only the visible subtitle rectangle, and records its original placement via `offsetX` and `offsetY`.\n- `screen` preserves the full ASS presentation dimensions for stable test fixtures or full-frame exports.\n\n## Fonts\n\n`rwss` registers browser-provided fonts into rassa's virtual font registry before opening a track.\n\n```ts\nimport { AssRenderer } from '@altqx/rwss'\n\nconst renderer = new AssRenderer({\n  video: videoElement,\n  subUrl: '/subtitles/movie.ass',\n  availableFonts: {\n    'Noto Sans CJK JP': '/fonts/NotoSansCJKjp-Regular.otf',\n    'Noto Sans': '/fonts/NotoSans-Regular.ttf'\n  },\n  fallbackFonts: ['Noto Sans CJK JP', 'Noto Sans'],\n  useLocalFonts: true\n})\n\nawait renderer.addFont('/fonts/Custom.ttf')\nrenderer.setDefaultFont('Noto Sans CJK JP')\n```\n\nFont inputs can be URLs, `Uint8Array`/`ArrayBuffer` bytes, or `RwssFontSource` objects with explicit `name`, `aliases`, `style`, and `isFallback` metadata. TTF, OTF, TTC, OTC, WOFF, and WOFF2 inputs are accepted. Full-name and PostScript aliases are discovered automatically; an explicitly supplied attachment takes priority over an `availableFonts` fallback with the same family.\n\n## Encrypted subtitle transport\n\n`rwss` can accept AES-GCM encrypted subtitle content where each chunk is IV-prefixed. Raw 128/192/256-bit key bytes or `CryptoKey` objects are accepted:\n\n```ts\nimport { AssRenderer, createEncryptedSubtitleContent, importAesGcmKey } from '@altqx/rwss'\n\nconst key = await importAesGcmKey(rawKeyBytes)\nconst encryptedSubContent = await createEncryptedSubtitleContent(assText, key)\n\nconst renderer = new AssRenderer({\n  video: videoElement,\n  encryptedSubContent\n})\n```\n\nYou can replace tracks at runtime:\n\n```ts\nrenderer.setTrackByUrl('/subtitles/episode-02.ass')\nrenderer.setTrack(assText)\nrenderer.setEncryptedTrack(encryptedSubContent)\nrenderer.freeTrack()\n```\n\n## Runtime ASS editing\n\nThe high-level renderer exposes AkariSub-like event/style mutation helpers. Changes rebuild the in-memory ASS document and reopen the WASM parser:\n\n```ts\nrenderer.createEvent({\n  Start: 1.0,\n  Duration: 2.5,\n  Style: 'Default',\n  Text: 'Hello from rwss'\n})\n\nrenderer.setEvent({ Text: 'Edited line' }, 0)\nrenderer.removeEvent(0)\n\nrenderer.createStyle({ Name: 'Large', FontName: 'Noto Sans', FontSize: 48 })\nrenderer.styleOverride({ FontSize: 42 })\nrenderer.disableStyleOverride()\n```\n\n## Observability events and stats\n\nUse `onEvent` and `getStats()` to inspect renderer behavior:\n\n```ts\nconst renderer = new AssRenderer({\n  video: videoElement,\n  subUrl: '/subtitles/movie.ass',\n  onEvent: (event) => {\n    switch (event.type) {\n      case 'load-start':\n      case 'load-complete':\n      case 'track-ready':\n      case 'render':\n      case 'error':\n      case 'message':\n        console.log(event)\n        break\n    }\n  }\n})\n\nconsole.log(await renderer.getStats())\nconsole.log(renderer.getStatsSnapshot())\nawait renderer.resetStats()\n```\n\n`PerformanceStats` includes frame counts, render timing, learned `timingCompensationMs`, last image counts, render FPS, cache counters, and worker/offscreen/on-demand flags.\n\n`AssRenderer` also implements `EventTarget`, so AkariSub-style `ready`, `trackReady`, `partial_ready`, and `error` events are available alongside `onEvent`.\n\n## GPU composition helpers\n\nVideo-managed overlays use the bounds-cropped WASM + Canvas2D path by default. Set both `offscreenRender: true` and `rawAssImageGpu: true` to opt into direct WebGL2 plane presentation. For lower-level integrations, `WebGPURenderer` and `WebGL2Renderer` expose ASS image-plane composition surfaces:\n\n```ts\nimport { WebGPURenderer, WebGL2Renderer, isWebGPUSupported, isWebGL2Supported } from '@altqx/rwss'\n\nconsole.log({ webgpu: isWebGPUSupported(), webgl2: isWebGL2Supported() })\n\nconst gpu = new WebGPURenderer(canvas)\nawait gpu.present(assSubtitleData) // direct swapchain presentation\nconst result = await gpu.renderAsync(assSubtitleData) // explicit readback\n\nconst gl = new WebGL2Renderer(canvas)\ngl.present(assSubtitleData) // direct framebuffer presentation\nconst glResult = gl.render(assSubtitleData) // explicit readback\n```\n\nBoth GPU helpers keep their readback APIs for export/tests while `present()` avoids the synchronous GPU-to-CPU path. Unsupported/headless environments retain deterministic CPU composition.\n\n## Worker/offscreen rendering\n\n`AssRendererWorkerClient` provides a worker protocol for OffscreenCanvas handoff:\n\n```ts\nimport { createAssRendererWorkerClient } from '@altqx/rwss'\n\nconst client = createAssRendererWorkerClient({\n  canvas: offscreenCanvas,\n  options: {\n    subUrl: '/subtitles/movie.ass',\n    offscreenRender: true\n  },\n  onEvent: (event) => console.log(event)\n})\n\nawait client.ready\nawait client.renderAt(videoElement.currentTime)\nclient.destroy()\n```\n\n## Notes\n\n- `rwss` handles ASS/SSA only. It does not parse bitmap subtitle formats such as PGS or VobSub; use `libbitsub` for those.\n- `rwss` uses WASM as the single subtitle engine path. There is no JavaScript renderer switch or HarfBuzz GPU glyph mode.\n- The default packaged fallback font is Liberation Sans (`src/default.woff2`). Configure additional fonts for CJK or custom-styled tracks.\n- The project tracks AkariSub browser orchestration from `altqx/akarisub` main after v0.2.2: exact-frame timelines, prefetch runways, adaptive timing, GPU fallback, and worker protocol surfaces.\n\n## API Reference\n\n### Top-level exports\n\n- `initWasm(input?, glueUrl?): Promise<WasmModule>` initializes the generated WASM package. With no arguments it imports `pkg/rwss.js` and lets wasm-bindgen resolve the `.wasm` asset. Called automatically by high-level and parser APIs; safe to call multiple times.\n- `isWasmInitialized(): boolean` reports whether initialization has completed.\n- `getWasm()` returns the initialized WASM module or throws if not initialized.\n- `getWasmUrl()` returns the package `rwss_bg.wasm` URL resolved from `import.meta.url`.\n- `getWasmGlueUrl()` returns the package `rwss.js` glue URL resolved from `import.meta.url`.\n- `openAss(text, wasmUrl?): Promise<OpenedAssSubtitles>` opens an ASS/SSA document.\n- `detectSubtitleFormat(nameOrText): 'ass' | 'unknown'` detects ASS/SSA by extension or text header.\n- `renderFrameData(frame, options?): AssRenderedFrameData` composes an ASS frame into RGBA pixels.\n- `toCanvas(frame, target?, options?)`, `toImageBitmap(frame, options?)`, and `toBlob(frame, type?, quality?, options?)` export rendered frame data.\n- `registerFont(...)`, `registerAvailableFonts(...)`, `registerFontBytes(...)`, `registerFontData(...)`, `setFallbackFonts(...)`, `clearRegisteredFonts()`, `listRegisteredFonts()`, and `resolveFont(...)` manage the virtual font registry.\n- `createEncryptedSubtitleContent(...)`, `decryptSubtitleContent(...)`, and `importAesGcmKey(...)` handle encrypted subtitle payloads.\n- `createAssRenderer(options)` constructs a high-level `AssRenderer`.\n- `WebGPURenderer`, `WebGL2Renderer`, `isWebGPUSupported()`, and `isWebGL2Supported()` expose GPU composition helpers.\n\n### `AssRenderer`\n\n- `constructor(options: VideoAssSubtitleOptions)` creates a video/canvas renderer.\n- `load(): Promise<void>` loads the configured subtitle input.\n- `start(): void` starts the render loop.\n- `stop(): void` stops the render loop.\n- `resize(width?, height?, top?, left?, force?): void` resizes the overlay canvas.\n- `setVideo(video): void` updates the backing video element.\n- `setFrameTimeline(frameTimes): void` replaces or disables the encoded-frame timeline.\n- `setTrackByUrl(url): void`, `setTrack(content): void`, `setEncryptedTrack(content): void`, and `freeTrack(): void` manage subtitle tracks.\n- `rendererType`, `isUsingGPURenderer`, `framePrefetch`, `renderAhead`, and `timeOffset` expose live renderer state.\n- `setCurrentTime(isPaused?, currentTime?, rate?, force?): void`, `setIsPaused(isPaused): void`, and `setRate(rate): void` control manual playback state.\n- `createEvent(event)`, `setEvent(event, index)`, `removeEvent(index)`, and `getEvents()` edit/read events.\n- `createStyle(style)`, `setStyle(style, index)`, `removeStyle(index)`, `getStyles()`, `styleOverride(style)`, and `disableStyleOverride()` edit/read styles.\n- `addFont(font, data?)` and `setDefaultFont(font)` manage fonts at runtime.\n- `getStats()`, `getStatsSnapshot()`, `resetStats()`, `getEventCount()`, and `getStyleCount()` expose diagnostics.\n- `runBenchmark()` emits a simple render timing message.\n- `destroy(): void` disposes parser state and removes auto-created overlay canvas.\n\n### `OpenedAssSubtitles`\n\n- `format`, `metadata`, and `timestamps` expose track metadata.\n- `renderAtIndex(index)` and `renderAtTimestamp(timeSeconds)` return ASS plane composition data.\n- `renderFrameDataAtIndex(index, options?)` and `renderFrameDataAtTimestamp(timeSeconds, options?)` return flattened frame exports.\n- `getEvents()` and `getStyles()` expose parsed ASS state.\n- `clearCache()` clears parser-side render caches.\n- `dispose()` frees parser resources.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}