# ogv.js: decoding Ogg and WebM in the browser with WebAssembly, one codec library at a time

> ogv.js is a browser media player built by compiling libogg, libvorbis, libtheora, libopus, libvpx, libnestegg and dav1d to WebAssembly with Emscripten. It exists for the browsers that cannot play those formats natively, and its own roadmap shows both how far it has come and what it still cannot do.

**bvibber/ogv.js** — JavaScript media player using Ogg/Vorbis/Theora/Opus/WebM libs compiled with Emscripten

- Repository: https://github.com/bvibber/ogv.js
- Website: https://brooke.vibber.net/misc/ogv.js/demo/
- Stars: 1,238 · Forks: 105
- Language: JavaScript
- License: NOASSERTION
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/bvibber-ogv-js

## The gap it fills is iOS Safari, where every browser is Safari

ogv.js is a media decoder and player for Ogg Vorbis, Opus and Theora, and for WebM carrying VP8, VP9 or AV1 video. The reason it exists is a specific hole rather than a general dissatisfaction with HTML5 video. The README notes that Windows and macOS can support Ogg and WebM by installing codecs or using alternate browsers with built-in support, but that this is not possible on iOS, where all browsers are really Safari. That single sentence defines the project's scope. The long term goal stated in the README is a drop-in replacement for the HTML5 video and audio tags that can handle Ogg Theora and Vorbis or WebM media on browsers with no native support. The feature list backs the goal: streaming with a Range header, seeking for Ogg and WebM with a Range header, color, audio with a/v sync, background threading for the video and audio decoders in Workers, and WebAssembly with an asm.js fallback. What is missing from that list is as telling as what is in it, most obviously the controls line, which says the controls are currently provided by the demo or another UI harness.

## Eight Emscripten modules, one per codec path

The Makefile shows the structure better than any description could. Eight Emscripten module targets are built: `ogv-demuxer-ogg.js`, `ogv-demuxer-webm.js`, `ogv-decoder-audio-vorbis.js`, `ogv-decoder-audio-opus.js`, `ogv-decoder-video-theora.js`, `ogv-decoder-video-vp8.js`, `ogv-decoder-video-vp9.js` and `ogv-decoder-video-av1.js`. The pattern is a demuxer layer and a decoder layer, compiled from C sources under `src/c` and wrapped by JavaScript modules under `src/js/modules`. The video decoders for VP8, VP9 and AV1 come from libvpx and dav1d, and the WebM container is read through libnestegg, while the Ogg side uses libogg with libvorbis, libopus and libtheora. Rendering happens through yuv-canvas with WebGL, which the README lists as GPU accelerated drawing. The same list makes the cost model clear: no GPU accelerated decoding, and no SIMD acceleration, so the WebAssembly code carries the video work on the CPU. When the `MT` variable is defined, two extra multithreaded targets are added, `ogv-decoder-video-vp9-mt.js` and `ogv-decoder-video-av1-mt.js`.

## The build runs through webpack and a Makefile of Emscripten targets

Two build systems sit side by side here. The Makefile is the outer layer, with variables for the demo, tests and build scripts directories and targets for the Emscripten modules, while package.json drives the JavaScript side. The npm scripts are `build` for webpack, `lint` for `eslint src/js`, `test` for `make tests` followed by `npx static build/tests`, and two dev server scripts, `demo` and `start`, both based on `webpack serve`. The `prepublish` script is `make -j4 dist`, which means publishing the package triggers a full native build rather than shipping prebuilt artifacts from the repository. The npm package itself is small in terms of what it declares: the `files` array lists `index.js`, `README.md`, `COPYING` and `dist/`, and the main entry is `index.js`. The webpack configuration file sits at the top level, and the dev dependencies pin webpack 5, babel, eslint and yuv-canvas. The declared browser support target in package.json is `> 5%`, which is a broad share, but the compatibility section in the README names a much narrower practical range.

## Installing from npm and pointing the loader at your resources

Pre-built releases are available as zip downloads from the GitHub releases page and through npm. The install is a single command, and the distribution-ready files end up in `node_modules/ogv/dist`.

```bash
npm install ogv
```

You can load the main entry point directly in a script tag, or bundle it with whatever build process you prefer, but the other JavaScript files have to be available for runtime loading, together in the same directory. The loader tries to auto-detect the path to its resources from the script element that loads `ogv.js` or `ogv-support.js`, and if you load it through another bundler such as browserify or MediaWiki's ResourceLoader you may have to override that manually before creating players. The browserify and webpack snippet in the README shows the three steps: require the package, set the base path, then construct a player.

```javascript
var ogv = require('ogv');

// Access public classes either as ogv.OGVPlayer or just OGVPlayer.
// Your build/lint tools may be happier with ogv.OGVPlayer!
ogv.OGVLoader.base = '/path/to/resources';
var player = new ogv.OGVPlayer();
```

The `OGVPlayer` class implements a player and supports a subset of the events, properties and methods from HTMLMediaElement and HTMLVideoElement. That subset is the second half of the integration cost: a harness that expects the full media element API will find gaps.

## Threaded decoding needs opt in HTTP headers you probably do not control

Multithreaded decoding is the part of the roadmap with the sharpest operational edge. The README describes it as in development, enabled by setting `options.threading` to `true`, and notes that it requires flags to be enabled in Firefox 65 and Chrome 72, with no support in Safari yet. The underlying constraint is `SharedArrayBuffer`, which needs special opt-in HTTP headers. The demo's `threaded.php` provides a COOP-COEP-CORP environment for testing threaded decoders, and the 1.8.7 changelog entry spells out the requirement in detail: the top-level frame and all worker JavaScript must opt in to COOP-COEP, and CORP or CORS is required for most loaded resources. The 2.0.0 notes in the README add that multithreaded versions are available experimentally and that this requirement still stands. For a site on a shared CDN or a host that does not set those headers on every response, the threaded path is simply off, and the single threaded VP8, VP9 and AV1 targets have to carry the work.

## Packaging with webpack is the documented weak point

The status section of the README is candid in a way that is worth reading before you adopt. It says that as of 2021 ogv.js works pretty nicely but may still have some packaging oddities with tools like webpack, and that it should work via CDNs again as of 1.8.2 if you cannot or do not want to package locally, but that this is not documented well yet. It also says improved documentation will come with the next major update and code cleanup. A separate changelog entry for 1.8.3 mentions a provisional fix for `OGVLoader.base` not working with CDNs, and admits that the fallback code for loading a non-local worker had been broken with WebAssembly for some time. The pattern across those entries is consistent: the decoder core works, and the friction is in how the resources are located and loaded. A build pipeline that rewrites script paths, or a content security policy that blocks workers, is where this project has historically broken.

## Native playback is the better option wherever it exists

The genuine alternative to ogv.js is not another JavaScript player, it is the browser's own decoder. The README names the testing browsers as Firefox 65 and Chrome 73 precisely because those support `.ogv` and `.webm` natively, and the primary targets are Safari 6.1 to 12 on macOS and 64-bit Safari on iOS 10 to 11, where the format support is missing. The difference in approach is fundamental. A native decoder runs in the platform, uses the hardware where the platform provides it, and gives you the full HTMLMediaElement surface. ogv.js gives you a WebAssembly port of the same libraries, with a partial API, no built-in controls, and CPU bound video decoding. So the decision is mostly a question of the platform your audience is on: on iOS, ogv.js is close to the only option, and the Wikimedia Commons deployment described in the README, running on Wikipedia and Wikimedia Commons in Safari and IE/Edge since August 2015 through the MediaWiki TimedMediaHandler integration, is the model case. Elsewhere, shipping WebM or Ogg to a modern browser is wasted effort.

## Conclusion

Adopt ogv.js when your users are on iOS Safari, or on a browser without native Ogg or WebM support, and the media you serve is in those formats. Do not adopt it for a general purpose web player: there are no built-in controls, no SIMD path, no GPU accelerated decoding, and the framework's own status note warns about packaging quirks with webpack. Before you ship it, confirm that the build you package actually loads its runtime resources, because the loader auto-detects the path and a bundler can break that, and set OGVLoader.base explicitly if it does. The npm package declares the MIT licence, the last push was on 2026-06-23, and the newest published release is 1.9.0 from 2024-04-23, with 2.0.0 still marked as in development.

## FAQ

### Which formats can ogv.js decode?

Ogg Vorbis, Opus and Theora audio and video, and WebM carrying VP8, VP9 or AV1 video. The libraries behind it are libogg, libvorbis, libtheora, libopus, libvpx, libnestegg and dav1d, all compiled to JavaScript and WebAssembly with Emscripten.

### How do I install ogv.js?

Run `npm install ogv`, and the distribution-ready files appear in `node_modules/ogv/dist`. Pre-built releases are also downloadable as zip files from the GitHub releases page, and the main entry point can be loaded directly in a script tag.

### Why does my ogv.js build fail to find its resources?

The loader auto-detects the resource path from the script element that loads ogv.js or ogv-support.js, so a bundler can break it. The README says you may need to set `ogv.OGVLoader.base` manually, and notes that packaging oddities with tools like webpack are a known issue.

### Does ogv.js support multithreaded decoding?

It is listed as in development, enabled with `options.threading` set to `true`, and it relies on SharedArrayBuffer, which requires opt-in COOP-COEP headers on the top-level frame and all worker JavaScript. The demo's `threaded.php` provides that environment for testing, and Safari is not supported yet.

### Does ogv.js provide playback controls?

No. The feature list states that controls are currently provided by the demo or another UI harness, and the `OGVPlayer` class supports only a subset of the events, properties and methods of HTMLMediaElement and HTMLVideoElement.

## Sources

- [bvibber/ogv.js on GitHub](https://github.com/bvibber/ogv.js)
- [Issues](https://github.com/bvibber/ogv.js/issues)
- [Project website](https://brooke.vibber.net/misc/ogv.js/demo/)
- [README](https://github.com/bvibber/ogv.js/blob/main/README.md)
- [Releases](https://github.com/bvibber/ogv.js/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bvibber-ogv-js
