React doesn't ship a video player. It ships nothing at all for video. <video src="movie.mp4" controls /> is the whole native story, and that element plays HLS only where the browser implements it: Safari and iOS have always, desktop Chrome since version 142, Firefox not at all. Every library below exists to close some part of that gap, and they close very different parts. Picking one by GitHub stars is how teams end up with a player that renders beautifully and can't decrypt a DRM licence.
TL;DR
- React has no player. The HTML5
<video>element plays progressive MP4. Adaptive streaming over HLS needs Media Source Extensions, which needs a library, except where the browser plays HLS itself: Safari and iOS, and desktop Chrome from version 142. - The libraries split into three jobs. Playback engines parse manifests and feed the video element. UI layers draw controls. Full players do both, plus DRM, captions and analytics.
- hls.js is the engine almost everything else uses. It has no interface at all.
- video.js, Plyr and Vidstack give you an interface. What they give you underneath varies, and two of them still need a separate engine for HLS.
- shaka-player carries DRM through the Encrypted Media Extensions API, which is the axis most comparisons skip.
- The real question isn't which library. It's how much of the surrounding pipeline you plan to build. Manifests, renditions, token refresh, and the analytics that tell you the player is failing.
What a React video player actually has to do
Playing video in a browser is four jobs stacked on each other, and a "player library" might do any subset of them.
The first is protocol. An adaptive stream is a text manifest listing several renditions. Something has to parse it, pick a rendition, fetch segments, and append them to a SourceBuffer through Media Source Extensions. Safari and iOS do this natively for HLS, and desktop Chrome has since version 142. Firefox doesn't.
The second is interface: play, scrub, volume, fullscreen, captions, quality selection, keyboard shortcuts and focus management. The third is protection, meaning signed URLs, and beyond that DRM through Encrypted Media Extensions with a licence server. The fourth is observability, which is the one teams discover last, usually after a customer reports buffering nobody can reproduce.
| Library | Version | Licence | HLS without extra deps | Ships a UI | DRM | Analytics included |
|---|---|---|---|---|---|---|
| hls.js | 1.7.3 | Apache-2.0 | Yes, that is the job | No | Via EME hooks | No |
| video.js | 8.24.0 | Apache-2.0 | Yes, via bundled http-streaming | Yes, skinnable | Via plugin | No |
| shaka-player | 5.2.10 | Apache-2.0 | Yes, plus DASH | Separate UI library | Yes, built in | No |
| Plyr | 3.8.4 | MIT | No, wraps an engine you supply | Yes | No | No |
| Vidstack (@vidstack/react) | 0.6.15 | MIT | Yes, via provider loaders | Yes, headless primitives | Provider-dependent | No |
| react-player | 3.4.0 | MIT | Via a per-provider dependency | Inherits the provider's | Provider-dependent | No |
| @fastpix/fp-player | 1.0.22 | MIT | Yes | Yes | Yes | Yes |
hls.js: the engine under most of the others
hls.js is an HLS client built on Media Source Extensions, and it's deliberately not a player. It hands you a stream attached to a <video> element, plus a large event surface for level switching, fragment loading and error recovery. Controls, captions UI, quality menus and analytics are all yours to build.
In React that means a useRef on the video element, a useEffect that constructs the instance, and a cleanup function that calls destroy(). Skipping the cleanup is the classic leak. React 18 StrictMode mounts effects twice in development, and you end up with two instances fighting over one SourceBuffer. We wrote the lifecycle up in detail in playing HLS in React with hls.js.
Limitations: no user interface of any kind and no DASH support. Hls.isSupported() is true on macOS Safari and on iOS 17.1 and later, so hls.js runs there too; the native video.src path is only needed for iPhones before 17.1, and every production integration carries that second branch.
video.js: the plugin ecosystem
video.js is an HTML5 player with its own skin and a plugin architecture that has accumulated well over a decade of extensions. HLS and DASH playback come from the http-streaming project bundled into version 8, so a single install covers protocol and interface together. The React integration is a wrapper: instantiate the player against a ref in an effect, call dispose() on unmount.
Limitations: the plugin ecosystem is the product, so capability depends on which plugins are maintained, and DRM specifically comes from a separate plugin rather than core. The default skin is recognisable enough that restyling it to look unbranded is real work. Maintenance sits largely with a commercial vendor's staff, which is public in the npm maintainer list.
shaka-player: the DRM path
shaka-player is Google's open-source player library, describing itself as a DASH and EME player, with HLS support alongside. DRM is core rather than a plugin, which is the distinguishing feature. Widevine, PlayReady and FairPlay licence acquisition are configured through a drm.servers map, and offline storage is part of the library.
Limitations: the controls live in a separate UI library, so the default install gives you playback and no interface. The configuration surface is wide, which is the cost of covering DASH, HLS and three DRM systems in one API.
Plyr: the interface, not the engine
Plyr is a styled, accessible interface for HTML5 video, YouTube and Vimeo. It doesn't parse manifests. For HLS you instantiate hls.js yourself, attach it to the same media element, and let Plyr draw the controls on top. plyr-react (6.0.0) is the React wrapper.
Limitations: no adaptive streaming of its own. A Plyr HLS setup is two libraries, and the quality menu needs wiring to the engine's level list by hand. No DRM.
Vidstack: headless primitives
Vidstack ships player primitives as web components with a React package, aimed at teams that want to compose their own controls rather than restyle someone else's. It loads provider-specific engines behind a common API.
Limitations: the latest tag still points at 0.6.15 from April 2024, while 1.x ships under the next tag. So the version you get from a plain npm install is two years behind the one under active development, and picking the newer line means opting into a pre-release tag.
react-player: many sources, one component
react-player takes a URL and plays it, whether that URL is an MP4, an HLS manifest, or a page on a third-party hosting service. Version 3 implements this by depending on a separate custom element per provider, which is visible in its dependency list.
Limitations: each provider is a different underlying implementation. The API surface you can rely on is the intersection of all of them, and per-provider behaviour differs underneath. The dependency count is a function of how many providers ship in the package rather than how many you use.
The axis nobody compares on
Every library above answers "can it play the stream". None of them answers "is it playing well", and that gap is where most player work actually goes.
A <video> element will happily tell you it's playing while a viewer watches a spinner. Detecting that needs startup time, rebuffering frequency and rebuffer duration, measured per session and aggregated across your audience. That's a data pipeline, not a player feature. FastPix Video Data collects those from the player through a set of monitors, one of them for hls.js. The engine you picked above doesn't have to change. The metric definitions are in the QoE metrics reference.
The FastPix web player takes the other route and includes it. See what those metrics look like on your own footage. The free plan covers 10 videos and 100K streaming minutes a month, no credit card.
It's a custom element rather than a React component, which works in React as a plain tag:
import "@fastpix/fp-player";
export default function Player({ playbackId }) {
return (
<fastpix-player
playback-id={playbackId}
stream-type="on-demand"
metadata-workspace-key={import.meta.env.VITE_FASTPIX_WORKSPACE_KEY}
/>
);
}Adding metadata-workspace-key turns on session analytics. Adding token plays a private stream. Neither is a plugin, and the monitoring attributes include the privacy controls, disable-cookies and respect-do-not-track, as attributes rather than configuration objects.
How to decide without a matrix
The libraries stop being interchangeable at three specific questions, and those questions are about your product rather than the library.
Do you control the encoder? If not, you don't control the rendition ladder either, and no player rescues a stream whose bottom rung is 4 Mbps.
Do you need to keep the video private? Signed URLs are a token in a query string and every library can carry one. DRM is different: it needs Encrypted Media Extensions support in the player itself, which narrows the list sharply.
Will anyone be on call for playback failures? If yes, you need session-level telemetry from the first release. A bug report that says "the video was buffering yesterday" is unactionable without it.
Answer those three and the shortlist usually collapses to one or two.
Try the FastPix player in your React app
Install @fastpix/fp-player, import it once for the side effect, and render <fastpix-player playback-id="..." stream-type="on-demand" /> anywhere in your tree. Adding metadata-workspace-key starts session analytics on the next play, and the first views appear in the dashboard within a minute. The free plan covers 10 videos and 100K streaming minutes a month, no credit card. Enough playback to compare startup times against whatever you run now.
Frequently Asked Questions (FAQs)
What is the best video player library for React?
There is no single answer, because the libraries do different jobs. hls.js is a playback engine with no interface. Video.js and Plyr provide an interface. Shaka Player carries DRM in core, and managed players bundle playback, DRM, and analytics together. Pick based on which of the four jobs—protocol, interface, protection, and observability—you intend to build yourself.
Can React play HLS natively?
No. React renders an HTML5 <video> element, and native HLS support in that element varies by browser: Safari and iOS have always had it, desktop Chrome gained it in version 142, and Firefox still does not. A library that implements HLS over Media Source Extensions covers all of them, which is what hls.js does. Check Hls.isSupported() first and keep a native fallback for iPhones before iOS 17.1, where it is false.
Do I need video.js or hls.js?
They solve different problems. Video.js version 8 actually bundles an HLS engine, so it is not an either-or choice in the way the question implies. hls.js alone gives you playback and no controls. Video.js gives you controls, a skin, and a plugin system on top of playback.
Which React video player supports DRM?
Shaka Player supports Widevine, PlayReady, and FairPlay in core through Encrypted Media Extensions. Video.js supports DRM through a separate plugin rather than core. Managed players, including the FastPix web player, handle license acquisition as part of playback, configured with a playback token rather than a license server URL.
How do I measure video playback quality in React?
Not from the player library, because none of them ship analytics. You need per-session startup time, rebuffering count, and rebuffer duration, collected from the playback engine and aggregated across viewers. FastPix Video Data does this with a monitor per engine: hls.js, Video.js, Shaka, and DASH.js. Collection does not depend on which player you chose.





