# OpenPlayerJS, a plugin-first HTML5 player that grew its own ad stack

> OpenPlayerJS v3 is a modular HTML5 video and audio player whose UI, engines and advertising are all plugins, and whose ad implementation no longer depends on the Google IMA SDK. The rebuild made the player easier to extend and cut support for DASH, FLV and core quality switching, so migration is the real cost of adopting it now.

**openplayerjs/openplayerjs** — Lightweight HTML5 video/audio player with smooth controls and ability to play VAST/VMAP/SIMID/OMID/non-linear ads

- Repository: https://github.com/openplayerjs/openplayerjs
- Website: https://www.openplayerjs.com/
- Stars: 649 · Forks: 91
- Language: TypeScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/openplayerjs-openplayerjs

## v3 rebuilt the player around plugins, not around a config object

OpenPlayerJS is a media player built on the HTML5 video and audio elements, and the project describes it as a media player that uses all the goods of HTML5 video and audio elements to play the most popular media in MP4 and MP3, HLS, and also has the ability to play VMAP, VAST, non-linear and companion ads.

What changed in v3 is the internal structure, and the project is direct about the scale of it, calling v3 a radical internal rebuild that keeps most of the player familiar to use but makes it much easier to extend, in controls, plugins and engines, and much easier to maintain.

The consequences are visible in the package table. The project is now a monorepo of scoped packages, and the table is the clearest description of the architecture.

| Package | Purpose |
| --- | --- |
| `@openplayerjs/core` | Player lifecycle, plugin system, engines, events |
| `@openplayerjs/player` | Default UI, built-in controls and UI extension APIs |
| `@openplayerjs/hls` | HLS engine, powered by hls.js |
| `@openplayerjs/ads` | VAST, VMAP, non-linear and companion ads plugin plus extension APIs |
| `@openplayerjs/youtube` | YouTube media engine plugin using the YouTube player API for iframe embeds |

So the plugin-first architecture means the UI, the ads and the engines are all plugins, and the core holds the player lifecycle, the plugin system, the engines and the events. The clean separation of concerns the project lists, core versus UI versus engines and plugins, is a package boundary, not a folder convention.

The audience is a publisher or a platform embedding video with advertising. The audience is not a developer who wants a five-line video tag, because there is a build step, a plugin set and a configuration surface to learn.

## The ads stack was rebuilt to escape the IMA SDK

The most consequential decision in v3 is not the packaging, it is the advertising.

The project states that ads are no longer using the Google IMA SDK, and that the team built their own with support using Dailymotion's open source libraries, and gives the reason: this was to support ads all around the world and to avoid geoblocking. That is a concrete engineering motivation rather than a general preference.

The ad protocols the player supports are the wide set: the topic list names VAST, VMAP, SIMID, OMID and non-linear. The package description for `@openplayerjs/ads` says it is a VAST, VMAP, non-linear and companion ads plugin with extension APIs, and the earlier project description mentions non-linear and companion ads alongside VMAP and VAST. The SIMID and OMID identifiers appear in the repository topics, and the package list names the ads package as covering VAST, VMAP, non-linear and companion ads.

Building your own ad stack is a serious undertaking, and the trade-off runs both ways. On the side of the decision: you control which demand sources are supported, you are not bound by a third party's regional availability, and you are not shipping a vendor SDK into every page. On the other side: you now own the correctness of ad parsing, tracking pixel behaviour, and the creative rendering, and the project had to absorb that maintenance cost. The v3 release notes frame the change as being about support and geoblocking rather than about capability, and the ads package remains separate so it can be versioned and shipped on its own.

The advertising package also carries extension APIs, which is what lets a publisher add their own ad UI without forking the plugin.

## What v3 dropped, and why that is the migration cost

The breaking changes are listed in the project documentation, and one of them is a decision worth arguing with.

MPEG-DASH and FLV support was dropped, in favour of just supporting what browsers natively support. The documentation is careful to add that if there is a need for them in the future, they can be added as separate bundles, like the HLS one.

That is a defensible principle. HLS is a separate bundle because it needs hls.js to polyfill a format Safari plays natively and Chrome does not. MPEG-DASH and FLV are not in that category: DASH has partial native support in some browsers, FLV has essentially none, and a player carrying a polyfill for either is carrying weight that most publishers do not need. But it is a real capability reduction, and anyone whose content is only served as DASH to a browser without native DASH support has to solve it outside the player.

The second change is core quality and levels support, dropped at the API level with `levels` and `level`, because quality is now engine-specific and has to be built inside each engine. The reasoning is sound in the v3 model, since a single `level` property cannot describe the quality ladder of an HLS variant playlist and a YouTube embed at the same time. The cost is that an application that used to switch quality through the core API now has to go through the engine that owns it.

The third is that several v2 mega-config options moved to the right package, whether UI, engine or plugin, and the documentation notes that this approach can work when using UMD files, depending on which plugins are enabled.

For anyone on v2, the migration document MIGRATION.v3.md is the thing to read first, and the documentation points at it directly.

## The markup is plain HTML, and the CORS is your problem

The setup is refreshingly HTML-first. All you need in your markup is a standard `<video>` or `<audio>` element with a `src` attached to it, with the `controls` and `playsinline` attributes added for cross-browser compatibility, and optionally `<source>` and `<track>` children.

```html
<html>
  <head>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@openplayerjs/player@latest/dist/openplayer.css" />
  </head>
  <body>
    <video id="player" class="op-player__media" controls playsinline>
      <source src="/path/to/video.mp4" type="video/mp4" />
      <track kind="subtitles" src="/path/to/video.vtt" srclang="en" label="English" />
    </video>
  </body>
</html>
```

The `op-player__media` class applies the base player styles, and the player's wrapper and controls are injected into the DOM automatically when `init()` is called. The stylesheet comes from jsDelivr, which is the simplest way to get the CSS without a build, and the same CDN is where the UMD bundle is served for the classic usage.

The bandwidth optimization section is the one piece of setup advice worth acting on. To avoid downloading media before the user presses play, you set `preload="none"` on the media element.

```html
<video id="player" class="op-player__media" controls playsinline preload="none">
  <source src="/video.mp4" type="video/mp4" />
</video>
```

The side effect is documented and it is not trivial. With `preload="none"` the player cannot read the media's duration until playback starts, so you have to provide the `duration` option if you want the UI to show the correct total time before the user presses play. A progress bar that shows a wrong total is a worse bug than a slow first load.

The other piece of setup work is server-side. When ads, captions or streaming manifests are loaded from a different domain, the server must allow cross-origin requests, and the typical error is a CORS policy block on the manifest fetch. The fix is response headers on your server.

```bash
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Credentials: true
```

And for public assets, allowing all origins.

```bash
Access-Control-Allow-Origin: *
```

This is the part of the setup that catches teams out, because the player works perfectly with same-origin media and only fails when the ad server or the CDN is on another host.

## A monorepo with turbo, pnpm and a real release process

The repository layout tells you that this is a monorepo with a mature toolchain, and the package.json root shows the shape of it.

The root package is `openplayerjs` at version 3.0.2, marked private, with the description HTML5 video and audio player, monorepo root, and the author Rafael Miranda. The licence is MIT, and the package type is module, so the whole thing is ES modules.

The build is a pnpm workspace with turbo driving the per-package tasks. `build` runs clean, then the type build, then turbo for bundles and css. `build:types` is `tsc -b tsconfig.references.json`, which is TypeScript project references across the packages, so a type error in one package fails the build of the packages that depend on it. There is a separate `build:bundles:affected` and `test:affected`, both turbo `--affected` variants, which is how a change in one package skips rebuilding the ones that do not depend on it.

The test setup is jest with coverage and a `--passWithNoTests` flag, and there is a Playwright config and an `e2e/` directory, so the browser-level tests are Playwright rather than jsdom. The `postinstall` script runs `playwright install chromium`, which means the browser is fetched on install rather than being an extra manual step.

The release process is scripted rather than manual. `release` runs `node scripts/orchestrate-release.cjs`, with `release:major`, `release:minor` and `release:patch` variants and a dry-run form of each. Configuration for the release automation is in `.release-it.cjs`, and commit messages follow conventional commits through `commitlint.config.cjs` with husky hooks installed by a `prepare` script. Renovate is enabled for dependency updates, and the badges show a linter workflow, a build workflow and a coverage report.

There is also an ARCHITECTURE.md at the root, which is where a monorepo of this size should put the explanation of how the packages fit together.

## UMD compatibility, examples, and the cost of a v2 upgrade

There is a compatibility path, and the project makes a point of it. If you use the UMD or CDN build today, you can keep the classic `new OpenPlayerJS('id', options); player.init();` flow with some minor changes, and the documentation has a dedicated UMD compatibility section for the v2 way of doing things.

That path is the escape hatch that makes v3 adoption less risky than the breaking changes suggest. A team that only needs the player and not the plugin architecture can stay on the classic API, while a team that wants to add a custom control or a new engine can move to the package-based setup.

The examples directory is unusually well stocked for a project of this size, and it is the fastest way to see what the features do. There is `basic.html`, `core.html`, `ads.html`, `captions.html`, `live.html`, `multiplugin.html`, `source-fallback.html`, `src-switch.html`, `youtube.html`, and `live-show-current-time.html`, plus a `captions/` subdirectory and an `ad-tags.js` file for the advertising example.

Source switching and source fallback are worth a look if you run a multi-CDN setup, since they are the features a publisher with unreliable delivery actually needs.

The release history shows a project in active maintenance with a recent cadence: v3.6.5 on 2026-09-01, v3.6.4 on 2026-08-25 and v3.6.3 on 2026-08-18. The last push to the repository was on 2026-10-19. Note the gap between the root package version 3.0.2 in the manifest and the 3.6.x tags on the individual published packages, which is a normal consequence of versioning packages separately in a monorepo but confusing if you expect the root version to mean something.

The licence is MIT, with a LICENSE file at the root, so there is no copyleft question to answer before you embed it.

## Conclusion

OpenPlayerJS v3 suits a team shipping ad-supported HTML5 video that needs VAST, VMAP, non-linear and companion ads without being tied to a single ad SDK's geography and vendor constraints, and it suits a team willing to run a monorepo build. It does not suit a project that needs MPEG-DASH or FLV out of the box, and it does not suit a team on the v2 configuration model, because several mega-config options moved into the package that owns them. Verify first that the packages you need exist for the browsers you support, that the ad server you use is reachable without the IMA SDK's regional restrictions, and that the CORS headers on your manifest and ad endpoints allow the origins you serve from.

## FAQ

### What is OpenPlayerJS and what media does it play?

It is an HTML5 video and audio player that plays MP4, MP3 and HLS, and it can play VMAP, VAST, non-linear and companion ads. It is built on the native HTML5 media elements rather than on a plug-in or a flash-era stack.

### Why did OpenPlayerJS v3 drop the Google IMA SDK?

The project states that ads no longer use the Google IMA SDK and that they built their own with support using Dailymotion's open source libraries, in order to support ads all around the world and avoid geoblocking.

### Does OpenPlayerJS v3 still support MPEG-DASH and FLV?

No. Both were dropped in v3 in favour of supporting what browsers natively support, with the note that if there is a need for them in the future they can be added as separate bundles, like the HLS one.

### How do I set up an OpenPlayerJS player in my page?

Add a standard video or audio element with a src, the controls and playsinline attributes and the op-player__media class, include the stylesheet from the CDN, and call init(). The player's wrapper and controls are injected into the DOM automatically.

### What happens if I set preload="none" on the OpenPlayerJS media element?

It avoids downloading media before the user presses play, but the player cannot read the media's duration until playback starts. To show the correct total time before play, you need to provide the duration option.

## Sources

- [License: MIT](https://github.com/openplayerjs/openplayerjs/blob/master/LICENSE)
- [openplayerjs/openplayerjs on GitHub](https://github.com/openplayerjs/openplayerjs)
- [Project website](https://www.openplayerjs.com/)
- [README](https://github.com/openplayerjs/openplayerjs/blob/master/README.md)
- [Releases](https://github.com/openplayerjs/openplayerjs/releases)

---

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