Update minimum browser support documentation and build config (#7880)

* Use Array.indexOf instead of Array.includes for legacy support

* Update the compatibility section of the README along with @babel/preset-env, es-check, and eslint configs

* Do not allow media timestamps (DTS) to map to negative values after timestampOffset is applied on remuxed TS (legacy browser support)

Resolves #7876
This commit is contained in:
Rob Walch
2026-05-29 09:15:21 -07:00
committed by GitHub
parent 8814807e8e
commit 819d90118c
10 changed files with 101 additions and 52 deletions
+38 -7
View File
@@ -1,7 +1,38 @@
{
"ecmaVersion": "es5",
"modules": "false",
"files": [
"./dist/**/*.js"
]
}
[
{
"ecmaVersion": "es5",
"module": false,
"files": [
"./dist/hls.js",
"./dist/hls.min.js",
"./dist/hls.light.js",
"./dist/hls.light.min.js",
"./dist/hls.worker.js"
]
},
{
"ecmaVersion": "es2016",
"module": false,
"checkFeatures": true,
"ignore": "globalThis,ErrorCause",
"files": [
"./dist/hls.light.js",
"./dist/hls.light.min.js",
"./dist/hls.worker.js"
]
},
{
"ecmaVersion": "es2017",
"module": false,
"checkFeatures": true,
"ignore": "globalThis,ErrorCause",
"files": ["./dist/hls.js", "./dist/hls.min.js"]
},
{
"ecmaVersion": "es2017",
"module": true,
"checkFeatures": true,
"ignore": "globalThis,ErrorCause",
"files": ["./dist/hls.mjs", "./dist/hls.light.mjs"]
}
]
+30 -30
View File
@@ -2,12 +2,23 @@ const asyncKeywordConstraintMsg =
'The async keyword adds a `regenerator` dependency in the hls.js ES5 output not allowed in v1 due to bundle size constraints.';
const selfVsWindowGlobalMsg =
'Use `self` instead of `window` to access the global context everywhere (including workers).';
const arrayFindCompatibilityMsg =
'Usage of Array find method is restricted for compatibility.';
const arrayFindIndexCompatibilityMsg =
'Usage of Array findIndex method is restricted for compatibility.';
const arrayEveryCompatibilityMsg =
'Usage of Array every method is restricted for compatibility. Use a negative Array some check instead.';
const arrayIncludesCompatibilityMsg =
'Usage of Array includes method is restricted for compatibility. Use Array indexOf instead. (For String includes, suppress this with an eslint-disable comment.)';
const baseRestrictedSyntax = [
{
selector: 'FunctionDeclaration[async=true]',
message: asyncKeywordConstraintMsg,
},
{
selector: 'ArrowFunctionExpression[async=true]',
message: asyncKeywordConstraintMsg,
},
{
selector: 'MethodDefinition[value.async=true]',
message: asyncKeywordConstraintMsg,
},
];
module.exports = {
env: { browser: true, commonjs: true, es6: true },
@@ -31,6 +42,7 @@ module.exports = {
// see https://github.com/standard/eslint-config-standard
// 'prettier' (https://github.com/prettier/eslint-config-prettier) must be last
extends: ['eslint:recommended', 'prettier'],
ignorePatterns: ['dist/'],
parser: '@typescript-eslint/parser',
parserOptions: { sourceType: 'module', project: './tsconfig.json' },
plugins: ['@typescript-eslint', 'import', 'no-for-of-loops'],
@@ -74,32 +86,11 @@ module.exports = {
'no-use-before-define': 'off',
'no-restricted-syntax': [
'error',
{
selector: 'FunctionDeclaration[async=true]',
message: asyncKeywordConstraintMsg,
},
{
selector: 'ArrowFunctionExpression[async=true]',
message: asyncKeywordConstraintMsg,
},
{
selector: 'MethodDefinition[value.async=true]',
message: asyncKeywordConstraintMsg,
},
...baseRestrictedSyntax,
{
selector:
'MemberExpression[property.name="find"][object.type="Identifier"]',
message: arrayFindCompatibilityMsg,
},
{
selector:
'MemberExpression[property.name="findIndex"][object.type="Identifier"]',
message: arrayFindIndexCompatibilityMsg,
},
{
selector:
'MemberExpression[property.name="every"][object.type="Identifier"]',
message: arrayEveryCompatibilityMsg,
'MemberExpression[property.name="includes"][object.type="Identifier"]',
message: arrayIncludesCompatibilityMsg,
},
],
'import/order': [
@@ -137,5 +128,14 @@ module.exports = {
'@typescript-eslint/no-misused-promises': 'error',
},
},
{
files: ['tests/**/*.ts'],
rules: {
// Tests run only in modern browsers and aren't bundled, so the
// Array.includes compatibility constraint that applies to src/ doesn't
// apply here.
'no-restricted-syntax': ['error', ...baseRestrictedSyntax],
},
},
],
};
+17 -6
View File
@@ -296,16 +296,27 @@ HLS.js is only compatible with browsers supporting MediaSource extensions (MSE)
HLS.js is supported on:
- Chrome 39+ for Android
- Chrome 39+ for Desktop
- Firefox 41+ for Android
- Firefox 42+ for Desktop
- Chrome 47+ for Desktop
- Firefox 51+ for Desktop
- Edge for Windows 10+
- Safari 9+ for macOS 10.11+
- Safari 10+ for macOS 10.11+
- Safari for iPadOS 13+
- Safari for iOS 17.1+ since HLS version [1.5.0](https://github.com/video-dev/hls.js/releases/tag/v1.5.0) using Managed Media Source (MMS) [WebKit blog](https://webkit.org/blog/14735/webkit-features-in-safari-17-1/)
- Chrome for Android 5+
- Firefox for Android 5+
A [Promise polyfill](https://github.com/taylorhakes/promise-polyfill) is required in browsers missing native promise support.
These versions are the targets passed to [`@babel/preset-env`](https://babeljs.io/docs/babel-preset-env) when building the UMD bundles in `dist/`. They share an **ES2016 runtime baseline**: ES5-style syntax plus native ES2016 globals (`Map`, `Set`, `Promise`, `Array.from`, `Uint8Array.from`, `Array.prototype.includes`, etc.). To keep bundle size small, no `core-js` polyfills are bundled.
Optional features such as CMCD pull in ES2017 APIs (e.g. `Object.entries`), so the full UMD bundle effectively requires an ES2017-capable runtime. The `light` bundle excludes those features and stays at the ES2016 baseline.
The `dist/` folder ships two distribution variants:
- **UMD** (`dist/hls.js`, `dist/hls.min.js`, `dist/hls.light.js`, `dist/hls.light.min.js`) — embeddable directly via a `<script>` tag (exposes a global `Hls`) or resolved by `require('hls.js')` via `package.json`'s `main` field. Targets the browser list above. The companion `dist/hls.worker.js` is the bundled transmuxer Web Worker.
- **ESM** (`dist/hls.mjs`, `dist/hls.light.mjs`) — resolved by `import 'hls.js'` via the `module` field. Built with `@babel/preset-env`'s `esmodules: true` target (≈ Chrome 61+, Firefox 60+, Safari 10.1+, Edge 16+) and intended to be consumed by a modern bundler. Uses ES2015+ syntax but stays below ES2019 (no `Array.prototype.flatMap`, `Object.fromEntries`, etc.).
If you import from `src/` directly or include any of our runtime dependencies untranspiled in your own build, you bypass this Babel pipeline and become responsible for transpilation; those source modules can reach for ES2019+ APIs that are tree-shaken out of the bundles we publish.
To run on browsers below this baseline, supply your own polyfills for any missing globals before HLS.js loads.
**Please note:**
+8 -3
View File
@@ -126,12 +126,17 @@ const workerFnFooter = '})(false);';
const extensions = ['.ts', '.js'];
// Browser baseline for the legacy UMD/IIFE bundles. Chosen so that all listed
// targets natively support the ES2015 built-ins (Map, Set, Array.from,
// Uint8Array.from, Promise, etc.) that source and bundled @svta/* deps rely
// on — we intentionally do NOT bundle core-js polyfills, to keep size down.
// Keep this list in sync with the "Compatibility" section in README.md.
const babelPresetEnvTargets = {
chrome: '47',
firefox: '51',
safari: '8',
ios: '8',
android: '4',
safari: '10',
ios: '10',
android: '5',
samsung: '5',
edge: '14',
};
+1
View File
@@ -1368,6 +1368,7 @@ class EMEController extends Logger implements ComponentAPI {
null,
new Uint16Array(licenseChallenge.buffer),
);
// eslint-disable-next-line no-restricted-syntax
if (!xmlString.includes('PlayReadyKeyMessage')) {
// This does not appear to be a wrapped message as on Edge. Some
// clients do not need this unwrapping, so we will assume this is one of
+1 -1
View File
@@ -360,7 +360,7 @@ export class ID3TrackController implements ComponentAPI {
if (id3Track && removeOldCues) {
if (id3Track.track.cues?.length) {
const idsToRemove = Object.keys(dateRangeCuesAppended).filter(
(id) => !ids.includes(id),
(id) => ids.indexOf(id) === -1,
);
for (let i = idsToRemove.length; i--; ) {
const id = idsToRemove[i];
+1 -1
View File
@@ -1789,7 +1789,7 @@ Schedule: ${scheduleItems.map((seg) => segmentToString(seg))} pos: ${this.timeli
if (
this.isInterstitial(playingItem) &&
removedIds.includes(playingItem.event.identifier)
removedIds.indexOf(playingItem.event.identifier) > -1
) {
this.warn(
`Interstitial "${playingItem.event.identifier}" removed while playing`,
+1 -1
View File
@@ -258,7 +258,7 @@ export class InterstitialsSchedule extends Logger {
const ids = Object.keys(dateRanges);
const removedInterstitials = previousInterstitialEvents
? previousInterstitialEvents.filter(
(event) => !ids.includes(event.identifier),
(event) => ids.indexOf(event.identifier) === -1,
)
: [];
if (interstitialEvents.length) {
+1 -1
View File
@@ -268,7 +268,7 @@ class SubtitleTrackController extends BasePlaylistController {
? this.hls.config.enableIMSC1
: this.hls.config.enableWebVTT
) {
if (!subtitleGroups || subtitleGroups.includes(track.groupId)) {
if (!subtitleGroups || subtitleGroups.indexOf(track.groupId) > -1) {
// track.id should match hls.subtitleTracks index
track.id = subtitleTracks.length;
subtitleTracks.push(track);
+3 -2
View File
@@ -245,7 +245,7 @@ export default class MP4Remuxer extends Logger implements Remuxer {
}
if (this.ISGenerated) {
if (enoughAudioSamples && enoughVideoSamples) {
if (enoughAudioSamples && enoughVideoSamples && timeOffset) {
// timeOffset is expected to be the offset of the first timestamp of this fragment (first DTS)
// if first audio DTS is not aligned with first video DTS then we need to take that into account
// when providing timeOffset to remuxAudio / remuxVideo. if we don't do that, there might be a permanent / small
@@ -482,7 +482,8 @@ export default class MP4Remuxer extends Logger implements Remuxer {
(timeOffset === 0 && videoInitPTS < _initPTS.baseTime)
) {
initDTS = Math.min(initDTS as number, videoInitDTS);
initPTS = Math.min(initPTS as number, videoInitPTS);
// Bind initPTS to the lowest DTS across tracks to prevent negative timestamps
initPTS = Math.min(initPTS as number, videoInitDTS);
}
}
this.videoTrackConfig = {