run 'npm run prettier'

This commit is contained in:
Tom Jenkinson
2020-12-23 20:15:35 +00:00
parent e0e786fdf7
commit 7342d4a6db
170 changed files with 8816 additions and 7598 deletions
+2 -2
View File
@@ -2,8 +2,8 @@
"source": "./src",
"destination": "./api-docs",
"plugins": [
{"name": "@itsjamie/esdoc-standard-plugin"},
{"name": "@itsjamie/esdoc-typescript-plugin", "option": {"enable": true}},
{ "name": "@itsjamie/esdoc-standard-plugin" },
{ "name": "@itsjamie/esdoc-typescript-plugin", "option": { "enable": true } },
{
"name": "@itsjamie/esdoc-ecmascript-proposal-plugin",
"option": {
+6 -6
View File
@@ -4,13 +4,13 @@
#### **Did you find a bug?**
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/video-dev/hls.js/issues).
- **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/video-dev/hls.js/issues).
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/video-dev/hls.js/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/video-dev/hls.js/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
#### **Did you write a patch that fixes a bug?**
- First, checkout the repository and install required dependencies
- First, checkout the repository and install required dependencies
```sh
git clone https://github.com/video-dev/hls.js.git
@@ -25,7 +25,7 @@ npm run lint
npm run test
```
- Use [EditorConfig](http://editorconfig.org/) or at least stay consistent to the file formats defined in the `.editorconfig` file.
- Develop in a topic branch, not master
- Use [EditorConfig](http://editorconfig.org/) or at least stay consistent to the file formats defined in the `.editorconfig` file.
- Develop in a topic branch, not master
Thanks! :heart: :heart: :heart:
Thanks! :heart: :heart: :heart:
+10 -4
View File
@@ -1,7 +1,6 @@
---
name: Bug report
about: Create a report to help us improve
---
### What version of Hls.js are you using?
@@ -9,11 +8,14 @@ about: Create a report to help us improve
### What browser and OS are you using?
### Test stream:
<!-- If possible, please provide a test stream or page -->
<!-- You can paste your stream into the demo and provide the permalink here -->
### Checklist
<!-- Replace [ ] with [x] to check off the list -->
- [ ] The issue observed is not already reported by searching on Github under https://github.com/video-dev/hls.js/issues
- [ ] The issue occurs in the stable client on https://hls-js.netlify.com/demo and not just on my page
<!-- The stable client is built from the latest release -->
@@ -23,20 +25,24 @@ about: Create a report to help us improve
- [ ] There are no network errors such as 404s in the browser console when trying to play the stream
### Steps to reproduce
1. Please provide clear steps to reproduce your problem
2. If the bug is intermittent, give a rough frequency
### Expected behavior
*What you expected to happen*
_What you expected to happen_
### Actual behavior
*What actually happened*
_What actually happened_
### Console output
```
Paste the contents of the browser console here.
```
```
For media errors reported on Chrome browser, please also paste the output of chrome://media-internals
For media errors reported on Chrome browser, please also paste the output of chrome://media-internals
```
@@ -1,7 +1,6 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
-1
View File
@@ -1,7 +1,6 @@
---
name: 'Question '
about: Need some help?
---
**What do you want to do with Hls.js?**
+1 -1
View File
@@ -6,6 +6,6 @@ updates:
interval: daily
commit-message:
# prevent netlify build
prefix: "[skip ci]"
prefix: '[skip ci]'
open-pull-requests-limit: 99
versioning-strategy: increase-if-necessary
+10 -10
View File
@@ -1,4 +1,4 @@
name: "CodeQL"
name: 'CodeQL'
on:
push:
@@ -17,15 +17,15 @@ jobs:
fail-fast: false
matrix:
language: ['javascript']
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+10 -12
View File
@@ -1,14 +1,12 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "lint",
"problemMatcher": [
"$eslint-stylish"
]
}
]
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "lint",
"problemMatcher": ["$eslint-stylish"]
}
]
}
+11 -11
View File
@@ -10,21 +10,21 @@ We pledge to act and interact in ways that contribute to an open, welcoming, div
Examples of behavior that contributes to a positive environment for our community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall community
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -67,7 +67,7 @@ Community leaders will follow these Community Impact Guidelines in determining t
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the project community.
+139 -118
View File
@@ -16,7 +16,7 @@ It works by transmuxing MPEG-2 Transport Stream and AAC/MP3 streams into ISO BMF
This transmuxing could be performed asynchronously using [Web Worker] if available in the browser.
hls.js also supports HLS + fmp4, as announced during [WWDC2016](https://developer.apple.com/videos/play/wwdc2016/504/)
hls.js does not need any player, it works directly on top of a standard HTML```<video>```element.
hls.js does not need any player, it works directly on top of a standard HTML`<video>`element.
hls.js is written in [ECMAScript6] (`*.js`) and [TypeScript] (`*.ts`) (strongly typed superset of ES6), and transpiled in ECMAScript5 using the [TypeScript compiler].
@@ -24,35 +24,37 @@ Modules written in TS and plain JS/ES6 can be interdependent and imported/requir
To build our distro bundle and serve our development environment we use [Webpack].
[HTML5 video]: https://www.html5rocks.com/en/tutorials/video/basics/
[MediaSource Extensions]: https://w3c.github.io/media-source/
[HTTP Live Streaming]: https://en.wikipedia.org/wiki/HTTP_Live_Streaming
[Web Worker]: https://caniuse.com/#search=worker
[ECMAScript6]: https://github.com/ericdouglas/ES6-Learning#articles--tutorials
[TypeScript]: https://www.typescriptlang.org/
[TypeScript compiler]: https://www.typescriptlang.org/docs/handbook/compiler-options.html
[Webpack]: https://webpack.js.org/
[html5 video]: https://www.html5rocks.com/en/tutorials/video/basics/
[mediasource extensions]: https://w3c.github.io/media-source/
[http live streaming]: https://en.wikipedia.org/wiki/HTTP_Live_Streaming
[web worker]: https://caniuse.com/#search=worker
[ecmascript6]: https://github.com/ericdouglas/ES6-Learning#articles--tutorials
[typescript]: https://www.typescriptlang.org/
[typescript compiler]: https://www.typescriptlang.org/docs/handbook/compiler-options.html
[webpack]: https://webpack.js.org/
## API docs and usage guide
* [API and usage docs, with code examples](./docs/API.md)
- [API and usage docs, with code examples](./docs/API.md)
* [Auto-Generated Docs (Latest Release)](https://hls-js.netlify.com/api-docs)
* [Auto-Generated Docs (Master)](https://hls-js-dev.netlify.com/api-docs)
- [Auto-Generated Docs (Latest Release)](https://hls-js.netlify.com/api-docs)
- [Auto-Generated Docs (Master)](https://hls-js-dev.netlify.com/api-docs)
_Note you can access the docs for a particular version using "[https://github.com/video-dev/hls.js/blob/deployments/README.md](https://github.com/video-dev/hls.js/blob/deployments/README.md)"_
## Demo
### Latest Release
[https://hls-js.netlify.com/demo](https://hls-js.netlify.com/demo)
### Master
[https://hls-js-dev.netlify.com/demo](https://hls-js-dev.netlify.com/demo)
### Specific Version
Find the commit on [https://github.com/video-dev/hls.js/blob/deployments/README.md](https://github.com/video-dev/hls.js/blob/deployments/README.md).
Find the commit on [https://github.com/video-dev/hls.js/blob/deployments/README.md](https://github.com/video-dev/hls.js/blob/deployments/README.md).
## Getting Started
@@ -111,9 +113,9 @@ see [this comment](https://github.com/video-dev/hls.js/pull/2954#issuecomment-67
//
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = videoSrc;
//
// If no native HLS support, check if hls.js is supported
//
//
// If no native HLS support, check if hls.js is supported
//
} else if (Hls.isSupported()) {
var hls = new Hls();
hls.loadSource(videoSrc);
@@ -124,46 +126,46 @@ see [this comment](https://github.com/video-dev/hls.js/pull/2954#issuecomment-67
## Video Control
Video is controlled through HTML ```<video>``` element.
Video is controlled through HTML `<video>` element.
HTMLVideoElement control and events could be used seamlessly.
## They use hls.js in production!
|[<img src="https://i.cdn.turner.com/adultswim/big/img/global/adultswim.jpg" width="120">](https://www.adultswim.com/streams)|[<img src="https://avatars3.githubusercontent.com/u/5497190?s=200&v=4" width="120">](https://www.akamai.com)|[<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/1a/Canal%2B.svg/2000px-Canal%2B.svg.png" width="120">](https://www.canalplus.fr)|[<img src="https://avatars2.githubusercontent.com/u/115313" width="120">](https://www.dailymotion.com)|
|---|---|---|---|
|[<img src="https://user-images.githubusercontent.com/4006693/44003595-baff193c-9e8f-11e8-9848-7bb91563499f.png" width="120">](https://freshlive.tv)|[<img src="https://flowplayer.org/media/img/logo-blue.png" width="120">](https://flowplayer.com)|[<img src="https://avatars1.githubusercontent.com/u/12554082?s=240" width="120">](https://www.foxsports.com.au)|[<img src="https://cloud.githubusercontent.com/assets/244265/12556435/dfaceb48-c353-11e5-971b-2c4429725469.png" width="120">](https://www.globo.com)|
|[<img src="https://images.gunosy.com/logo/gunosy_icon_company_logo.png" width="120">](https://gunosy.com)|[<img src="https://user-images.githubusercontent.com/1480052/35802840-f8e85b8a-0a71-11e8-8eb2-eee323e3f159.png" width="120">](https://www.gl-systemhaus.de/)|[<img src="https://cloud.githubusercontent.com/assets/6525783/20801836/700490de-b7ea-11e6-82bd-e249f91c7bae.jpg" width="120">](https://nettrek.de)|[<img src="https://cloud.githubusercontent.com/assets/244265/12556385/999aa884-c353-11e5-9102-79df54384498.png" width="120">](https://www.nytimes.com/)|
|[<img src="https://cloud.githubusercontent.com/assets/1798553/20356424/ba158574-ac24-11e6-95e1-1ae591b11a0a.png" width="120">](https://www.peer5.com/)|[<img src="https://cloud.githubusercontent.com/assets/4909096/20925062/e26e6fc8-bbb4-11e6-99a5-d4762274a342.png" width="120">](https://www.qbrick.com)|[<img src="https://www.radiantmediaplayer.com/images/radiantmediaplayer-new-logo-640.jpg" width="120">](https://www.radiantmediaplayer.com/)|[<img src="https://www.rts.ch/hummingbird-static/images/logos/logo_marts.svg" width="120">](https://www.rts.ch)|
|[<img src="https://cloud.githubusercontent.com/assets/12702747/19316434/0a3601de-9067-11e6-85e2-936b1cb099a0.png" width="120">](https://www.snapstream.com/)|[<img src="https://pamediagroup.com/wp-content/uploads/2019/05/StreamAMG-Logo-RGB.png" width="120">](https://www.streamamg.com/)|[<img src="https://streamsharkio.sa.metacdn.com/wp-content/uploads/2015/10/streamshark-dark.svg" width="120">](https://streamshark.io/)|[<img src="https://camo.githubusercontent.com/9580f10e9bfa8aa7fba52c5cb447bee0757e33da/68747470733a2f2f7777772e7461626c6f74762e636f6d2f7374617469632f696d616765732f7461626c6f5f6c6f676f2e706e67" width="120">](https://my.tablotv.com/)|
|[<img src="https://user-images.githubusercontent.com/2803310/34083705-349c8fd0-e375-11e7-92a6-5c38509f4936.png" width="120">](https://www.streamroot.io/)|[<img src="https://vignette1.wikia.nocookie.net/tedtalks/images/c/c0/TED_logo.png/revision/20150915192527" width="120">](https://www.ted.com/)|[<img src="https://www.seeklogo.net/wp-content/uploads/2014/12/twitter-logo-vector-download.jpg" width="120">](https://twitter.com/)|[<img src="https://player.clevercast.com/img/clevercast.png" width="120">](https://www.clevercast.com)|
|[<img src="https://player.mtvnservices.com/edge/hosted/Viacom_logo.svg" width="120">](https://www.viacom.com/)|[<img src="https://user-images.githubusercontent.com/1181974/29248959-efabc440-802d-11e7-8050-7c1f4ca6c607.png" width="120">](https://vk.com/)|[<img src="https://avatars0.githubusercontent.com/u/5090060?s=200&v=4" width="120">](https://www.jwplayer.com)|[<img src="https://staticftv-a.akamaihd.net/arches/francetv/default/img/og-image.jpg?20161007" width="120">](https://www.france.tv)|
|[<img src="https://showmax.akamaized.net/e/logo/showmax_black.png" width="120">](https://tech.showmax.com)|[<img src="https://static3.1tv.ru/assets/web/logo-ac67852f1625b338f9d1fb96be089d03557d50bfc5790d5f48dc56799f59dec6.svg" width="120" height="120">](https://www.1tv.ru/) | [<img src="https://user-images.githubusercontent.com/1480052/40482633-c013ebce-5f55-11e8-96d5-b776415de0ce.png" width="120">](https://www.zdf.de) | [<img src="https://github.com/cdnbye/hlsjs-p2p-engine/blob/master/figs/cdnbye.png" width="120">](https://github.com/cdnbye/hlsjs-p2p-engine)| |
|[<img src="https://streaming.cdn77.com/live-streaming-logo-dark.png" width="120">](https://streaming.cdn77.com/)| [<img src="https://avatars0.githubusercontent.com/u/7442371?s=200&v=4" width="120">](https://r7.com/)|[<img src="https://raw.githubusercontent.com/Novage/p2p-media-loader/gh-pages/images/p2pml-logo.png" width="120">](https://github.com/Novage/p2p-media-loader)|[<img src="https://avatars3.githubusercontent.com/u/45617200?s=400" width="120">](https://kayosports.com.au)
|[<img src="https://avatars1.githubusercontent.com/u/5279615?s=400&u=9771a216836c613f1edf4afe71cfc69d4c5657ed&v=4" width="120">](https://flosports.tv)
| [<img src="https://i.cdn.turner.com/adultswim/big/img/global/adultswim.jpg" width="120">](https://www.adultswim.com/streams) | [<img src="https://avatars3.githubusercontent.com/u/5497190?s=200&v=4" width="120">](https://www.akamai.com) | [<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/1a/Canal%2B.svg/2000px-Canal%2B.svg.png" width="120">](https://www.canalplus.fr) | [<img src="https://avatars2.githubusercontent.com/u/115313" width="120">](https://www.dailymotion.com) |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- |
| [<img src="https://user-images.githubusercontent.com/4006693/44003595-baff193c-9e8f-11e8-9848-7bb91563499f.png" width="120">](https://freshlive.tv) | [<img src="https://flowplayer.org/media/img/logo-blue.png" width="120">](https://flowplayer.com) | [<img src="https://avatars1.githubusercontent.com/u/12554082?s=240" width="120">](https://www.foxsports.com.au) | [<img src="https://cloud.githubusercontent.com/assets/244265/12556435/dfaceb48-c353-11e5-971b-2c4429725469.png" width="120">](https://www.globo.com) |
| [<img src="https://images.gunosy.com/logo/gunosy_icon_company_logo.png" width="120">](https://gunosy.com) | [<img src="https://user-images.githubusercontent.com/1480052/35802840-f8e85b8a-0a71-11e8-8eb2-eee323e3f159.png" width="120">](https://www.gl-systemhaus.de/) | [<img src="https://cloud.githubusercontent.com/assets/6525783/20801836/700490de-b7ea-11e6-82bd-e249f91c7bae.jpg" width="120">](https://nettrek.de) | [<img src="https://cloud.githubusercontent.com/assets/244265/12556385/999aa884-c353-11e5-9102-79df54384498.png" width="120">](https://www.nytimes.com/) |
| [<img src="https://cloud.githubusercontent.com/assets/1798553/20356424/ba158574-ac24-11e6-95e1-1ae591b11a0a.png" width="120">](https://www.peer5.com/) | [<img src="https://cloud.githubusercontent.com/assets/4909096/20925062/e26e6fc8-bbb4-11e6-99a5-d4762274a342.png" width="120">](https://www.qbrick.com) | [<img src="https://www.radiantmediaplayer.com/images/radiantmediaplayer-new-logo-640.jpg" width="120">](https://www.radiantmediaplayer.com/) | [<img src="https://www.rts.ch/hummingbird-static/images/logos/logo_marts.svg" width="120">](https://www.rts.ch) |
| [<img src="https://cloud.githubusercontent.com/assets/12702747/19316434/0a3601de-9067-11e6-85e2-936b1cb099a0.png" width="120">](https://www.snapstream.com/) | [<img src="https://pamediagroup.com/wp-content/uploads/2019/05/StreamAMG-Logo-RGB.png" width="120">](https://www.streamamg.com/) | [<img src="https://streamsharkio.sa.metacdn.com/wp-content/uploads/2015/10/streamshark-dark.svg" width="120">](https://streamshark.io/) | [<img src="https://camo.githubusercontent.com/9580f10e9bfa8aa7fba52c5cb447bee0757e33da/68747470733a2f2f7777772e7461626c6f74762e636f6d2f7374617469632f696d616765732f7461626c6f5f6c6f676f2e706e67" width="120">](https://my.tablotv.com/) |
| [<img src="https://user-images.githubusercontent.com/2803310/34083705-349c8fd0-e375-11e7-92a6-5c38509f4936.png" width="120">](https://www.streamroot.io/) | [<img src="https://vignette1.wikia.nocookie.net/tedtalks/images/c/c0/TED_logo.png/revision/20150915192527" width="120">](https://www.ted.com/) | [<img src="https://www.seeklogo.net/wp-content/uploads/2014/12/twitter-logo-vector-download.jpg" width="120">](https://twitter.com/) | [<img src="https://player.clevercast.com/img/clevercast.png" width="120">](https://www.clevercast.com) |
| [<img src="https://player.mtvnservices.com/edge/hosted/Viacom_logo.svg" width="120">](https://www.viacom.com/) | [<img src="https://user-images.githubusercontent.com/1181974/29248959-efabc440-802d-11e7-8050-7c1f4ca6c607.png" width="120">](https://vk.com/) | [<img src="https://avatars0.githubusercontent.com/u/5090060?s=200&v=4" width="120">](https://www.jwplayer.com) | [<img src="https://staticftv-a.akamaihd.net/arches/francetv/default/img/og-image.jpg?20161007" width="120">](https://www.france.tv) |
| [<img src="https://showmax.akamaized.net/e/logo/showmax_black.png" width="120">](https://tech.showmax.com) | [<img src="https://static3.1tv.ru/assets/web/logo-ac67852f1625b338f9d1fb96be089d03557d50bfc5790d5f48dc56799f59dec6.svg" width="120" height="120">](https://www.1tv.ru/) | [<img src="https://user-images.githubusercontent.com/1480052/40482633-c013ebce-5f55-11e8-96d5-b776415de0ce.png" width="120">](https://www.zdf.de) | [<img src="https://github.com/cdnbye/hlsjs-p2p-engine/blob/master/figs/cdnbye.png" width="120">](https://github.com/cdnbye/hlsjs-p2p-engine) | |
| [<img src="https://streaming.cdn77.com/live-streaming-logo-dark.png" width="120">](https://streaming.cdn77.com/) | [<img src="https://avatars0.githubusercontent.com/u/7442371?s=200&v=4" width="120">](https://r7.com/) | [<img src="https://raw.githubusercontent.com/Novage/p2p-media-loader/gh-pages/images/p2pml-logo.png" width="120">](https://github.com/Novage/p2p-media-loader) | [<img src="https://avatars3.githubusercontent.com/u/45617200?s=400" width="120">](https://kayosports.com.au) |
|[<img src="https://avatars1.githubusercontent.com/u/5279615?s=400&u=9771a216836c613f1edf4afe71cfc69d4c5657ed&v=4" width="120">](https://flosports.tv)
## Player Integration
hls.js is (being) integrated in the following players:
- [Akamai Adaptive Media Player (AMP)](https://www.akamai.com/us/en/solutions/products/media-delivery/adaptive-media-player.jsp)
- [Clappr](https://github.com/clappr/clappr)
- [Flowplayer](https://www.flowplayer.org) through [flowplayer-hlsjs](https://github.com/flowplayer/flowplayer-hlsjs)
- [MediaElement.js](https://www.mediaelementjs.com)
- [Videojs](https://videojs.com) through [Videojs-hlsjs](https://github.com/benjipott/videojs-hlsjs)
- [Videojs](https://videojs.com) through [videojs-hls.js](https://github.com/streamroot/videojs-hls.js). hls.js is integrated as a SourceHandler -- new feature in Video.js 5.
- [Videojs](https://videojs.com) through [videojs-contrib-hls.js](https://github.com/Peer5/videojs-contrib-hls.js). Production ready plug-in with full fallback compatibility built-in.
- [Fluid Player](https://www.fluidplayer.com)
- [OpenPlayerJS](https://www.openplayerjs.com), as part of the [OpenPlayer project](https://github.com/openplayerjs)
- [Akamai Adaptive Media Player (AMP)](https://www.akamai.com/us/en/solutions/products/media-delivery/adaptive-media-player.jsp)
- [Clappr](https://github.com/clappr/clappr)
- [Flowplayer](https://www.flowplayer.org) through [flowplayer-hlsjs](https://github.com/flowplayer/flowplayer-hlsjs)
- [MediaElement.js](https://www.mediaelementjs.com)
- [Videojs](https://videojs.com) through [Videojs-hlsjs](https://github.com/benjipott/videojs-hlsjs)
- [Videojs](https://videojs.com) through [videojs-hls.js](https://github.com/streamroot/videojs-hls.js). hls.js is integrated as a SourceHandler -- new feature in Video.js 5.
- [Videojs](https://videojs.com) through [videojs-contrib-hls.js](https://github.com/Peer5/videojs-contrib-hls.js). Production ready plug-in with full fallback compatibility built-in.
- [Fluid Player](https://www.fluidplayer.com)
- [OpenPlayerJS](https://www.openplayerjs.com), as part of the [OpenPlayer project](https://github.com/openplayerjs)
- [CDNBye](https://github.com/cdnbye/hlsjs-p2p-engine), a p2p engine for hls.js powered by WebRTC Datachannel.
## Chrome/Firefox integration
made by [gramk](https://github.com/gramk/chrome-hls), plays hls from address bar and m3u8 links
- Chrome [native-hls](https://chrome.google.com/webstore/detail/native-hls-playback/emnphkkblegpebimobpbekeedfgemhof)
- Firefox [native-hls](https://addons.mozilla.org/en-US/firefox/addon/native_hls_playback/)
- Chrome [native-hls](https://chrome.google.com/webstore/detail/native-hls-playback/emnphkkblegpebimobpbekeedfgemhof)
- Firefox [native-hls](https://addons.mozilla.org/en-US/firefox/addon/native_hls_playback/)
## Dependencies
@@ -175,7 +177,9 @@ If you want to bundle the application yourself, use node
```
npm install hls.js
```
or for the version from master (alpha)
```
npm install hls.js@alpha
```
@@ -206,15 +210,15 @@ Find a support matrix of the MediaSource API here: https://developer.mozilla.org
As of today, it is supported on:
* Chrome for Android 34+
* Chrome for Desktop 34+
* Firefox for Android 41+
* Firefox for Desktop 42+
* IE11+ for Windows 8.1+
* Edge for Windows 10+
* Opera for Desktop
* Vivaldi for Desktop
* Safari for Mac 8+ (beta)
- Chrome for Android 34+
- Chrome for Desktop 34+
- Firefox for Android 41+
- Firefox for Desktop 42+
- IE11+ for Windows 8.1+
- Edge for Windows 10+
- Opera for Desktop
- Vivaldi for Desktop
- Safari for Mac 8+ (beta)
Please note: iOS Safari "Mobile" does not support the MediaSource API. Safari browsers have however built-in HLS support through the plain video "tag" source URL. See the example above (Getting Started) to run appropriate feature detection and choose between using Hls.js or natively built-in HLS support.
@@ -232,88 +236,92 @@ All HLS resources must be delivered with [CORS headers](https://developer.mozill
## Features
- VoD & Live playlists
- DVR support on Live playlists
- fragmented MP4 container (beta)
- MPEG-2 TS container
- ITU-T Rec. H.264 and ISO/IEC 14496-10 Elementary Stream
- ISO/IEC 13818-7 ADTS AAC Elementary Stream
- ISO/IEC 11172-3 / ISO/IEC 13818-3 (MPEG-1/2 Audio Layer III) Elementary Stream
- Packetized metadata (ID3v2.3.0) Elementary Stream
- AAC container (audio only streams)
- MPEG Audio container (MPEG-1/2 Audio Layer III audio only streams)
- Timed Metadata for HTTP Live Streaming (in ID3 format, carried in MPEG-2 TS)
- AES-128 decryption
- SAMPLE-AES decryption (only supported if using MPEG-2 TS container)
- Encrypted media extensions (EME) support for DRM (digital rights management)
- Widevine CDM (beta/experimental) (see Shaka-package test-stream in demo)
- CEA-608/708 captions
- WebVTT subtitles
- Alternate Audio Track Rendition (Master Playlist with alternative Audio) for VoD and Live playlists
- Adaptive streaming
- Manual & Auto Quality Switching
- 3 Quality Switching modes are available (controllable through API means)
- Instant switching (immediate quality switch at current video position)
- Smooth switching (quality switch for next loaded fragment)
- Bandwidth conservative switching (quality switch change for next loaded fragment, without flushing the buffer)
- In Auto-Quality mode, emergency switch down in case bandwidth is suddenly dropping to minimize buffering.
- Accurate Seeking on VoD & Live (not limited to fragment or keyframe boundary)
- Ability to seek in buffer and back buffer without redownloading segments
- Built-in Analytics
- Every internal events could be monitored (Network Events,Video Events)
- Playback session metrics are also exposed
- Resilience to errors
- Retry mechanism embedded in the library
- Recovery actions could be triggered fix fatal media or network errors
- [Redundant/Failover Playlists](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/UsingHTTPLiveStreaming/UsingHTTPLiveStreaming.html#//apple_ref/doc/uid/TP40008332-CH102-SW22)
- VoD & Live playlists
- DVR support on Live playlists
- fragmented MP4 container (beta)
- MPEG-2 TS container
- ITU-T Rec. H.264 and ISO/IEC 14496-10 Elementary Stream
- ISO/IEC 13818-7 ADTS AAC Elementary Stream
- ISO/IEC 11172-3 / ISO/IEC 13818-3 (MPEG-1/2 Audio Layer III) Elementary Stream
- Packetized metadata (ID3v2.3.0) Elementary Stream
- AAC container (audio only streams)
- MPEG Audio container (MPEG-1/2 Audio Layer III audio only streams)
- Timed Metadata for HTTP Live Streaming (in ID3 format, carried in MPEG-2 TS)
- AES-128 decryption
- SAMPLE-AES decryption (only supported if using MPEG-2 TS container)
- Encrypted media extensions (EME) support for DRM (digital rights management)
- Widevine CDM (beta/experimental) (see Shaka-package test-stream in demo)
- CEA-608/708 captions
- WebVTT subtitles
- Alternate Audio Track Rendition (Master Playlist with alternative Audio) for VoD and Live playlists
- Adaptive streaming
- Manual & Auto Quality Switching
- 3 Quality Switching modes are available (controllable through API means)
- Instant switching (immediate quality switch at current video position)
- Smooth switching (quality switch for next loaded fragment)
- Bandwidth conservative switching (quality switch change for next loaded fragment, without flushing the buffer)
- In Auto-Quality mode, emergency switch down in case bandwidth is suddenly dropping to minimize buffering.
- Accurate Seeking on VoD & Live (not limited to fragment or keyframe boundary)
- Ability to seek in buffer and back buffer without redownloading segments
- Built-in Analytics
- Every internal events could be monitored (Network Events,Video Events)
- Playback session metrics are also exposed
- Resilience to errors
- Retry mechanism embedded in the library
- Recovery actions could be triggered fix fatal media or network errors
- [Redundant/Failover Playlists](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/UsingHTTPLiveStreaming/UsingHTTPLiveStreaming.html#//apple_ref/doc/uid/TP40008332-CH102-SW22)
## Not Supported (Yet)
- MP3 Elementary Stream in Edge for Windows 10+
- MP3 Elementary Stream in Edge for Windows 10+
### Supported M3U8 tags
For details on the HLS format and these tags meanings see https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-07
Manifest tags
- `#EXT-X-STREAM-INF:<attribute-list>`
`<URI>`
- `#EXT-X-MEDIA:<attribute-list>`
- `#EXT-X-SESSION-DATA:<attribute-list>`
- `#EXT-X-STREAM-INF:<attribute-list>`
`<URI>`
- `#EXT-X-MEDIA:<attribute-list>`
- `#EXT-X-SESSION-DATA:<attribute-list>`
Playlist tags
- `#EXTM3U`
- `#EXT-X-VERSION=<n>`
- `#EXTINF:<duration>,[<title>]`
- `#EXT-X-ENDLIST`
- `#EXT-X-MEDIA-SEQUENCE=<n>`
- `#EXT-X-TARGETDURATION=<n>`
- `#EXT-X-DISCONTINUITY`
- `#EXT-X-DISCONTINUITY-SEQUENCE=<n>`
- `#EXT-X-BYTERANGE=<n>[@<o>]`
- `#EXT-X-MAP:<attribute-list>`
- `#EXT-X-KEY:<attribute-list>`
- `#EXT-X-PROGRAM-DATE-TIME:<attribute-list>`
- `#EXT-X-START:TIME-OFFSET=<n>`
- `#EXT-X-SERVER-CONTROL:<attribute-list>`
- `#EXT-X-PART-INF:PART-TARGET=<n>`
- `#EXT-X-PART:<attribute-list>`
- `#EXT-X-PRELOAD-HINT:<attribute-list>`
- `#EXT-X-SKIP:<attribute-list>`
- `#EXT-X-RENDITION-REPORT:<attribute-list>`
- The following tags are added to their respective fragment's attribute list
- `#EXT-X-DATERANGE:<attribute-list>`
- `#EXT-X-BITRATE`
- `#EXT-X-GAP`
- `#EXTM3U`
- `#EXT-X-VERSION=<n>`
- `#EXTINF:<duration>,[<title>]`
- `#EXT-X-ENDLIST`
- `#EXT-X-MEDIA-SEQUENCE=<n>`
- `#EXT-X-TARGETDURATION=<n>`
- `#EXT-X-DISCONTINUITY`
- `#EXT-X-DISCONTINUITY-SEQUENCE=<n>`
- `#EXT-X-BYTERANGE=<n>[@<o>]`
- `#EXT-X-MAP:<attribute-list>`
- `#EXT-X-KEY:<attribute-list>`
- `#EXT-X-PROGRAM-DATE-TIME:<attribute-list>`
- `#EXT-X-START:TIME-OFFSET=<n>`
- `#EXT-X-SERVER-CONTROL:<attribute-list>`
- `#EXT-X-PART-INF:PART-TARGET=<n>`
- `#EXT-X-PART:<attribute-list>`
- `#EXT-X-PRELOAD-HINT:<attribute-list>`
- `#EXT-X-SKIP:<attribute-list>`
- `#EXT-X-RENDITION-REPORT:<attribute-list>`
- The following tags are added to their respective fragment's attribute list
- `#EXT-X-DATERANGE:<attribute-list>`
- `#EXT-X-BITRATE`
- `#EXT-X-GAP`
## License
hls.js is released under [Apache 2.0 License](LICENSE)
hls.js is released under [Apache 2.0 License](LICENSE)
## Development and contributing - first steps
Pull requests are welcome. Here is a quick guide on how to start.
- First, checkout the repository and install required dependencies
- First, checkout the repository and install required dependencies
```sh
git clone https://github.com/video-dev/hls.js.git
# setup dev environment
@@ -324,9 +332,10 @@ npm run dev
# lint
npm run lint
```
- Use [EditorConfig](https://editorconfig.org) or at least stay consistent to the file formats defined in the `.editorconfig` file.
- Develop in a topic branch, not master
- Don't commit the updated `dist/hls.js` file in your PR. We'll take care of generating an updated build right before releasing a new tagged version.
- Use [EditorConfig](https://editorconfig.org) or at least stay consistent to the file formats defined in the `.editorconfig` file.
- Develop in a topic branch, not master
- Don't commit the updated `dist/hls.js` file in your PR. We'll take care of generating an updated build right before releasing a new tagged version.
## Setup
@@ -339,28 +348,34 @@ npm install
## Build system (Webpack)
Build all flavors (suitable for prod-mode/CI):
```
npm install
npm run build
```
Only debug-mode artifacts:
```
npm run build:debug
```
Build and watch (customized dev setups where you'll want to host through another server than webpacks' - for example in a sub-module/project)
```
npm run build:watch
```
Only specific flavor (known configs are: debug, dist, light, light-dist, demo):
```
npm run build -- --env dist # replace "dist" by other configuration name, see above ^
```
Note: The "demo" config is always built.
Build with bundle analyzer (to help optimize build size)
```
npm run build:analyze
```
@@ -368,16 +383,19 @@ npm run build:analyze
## Linter (ESlint)
Run linter:
```
npm run lint
```
Run linter with auto-fix mode:
```
npm run lint:fix
```
Run linter with errors only (no warnings)
```
npm run lint:quiet
```
@@ -385,22 +403,25 @@ npm run lint:quiet
## Automated tests (Mocha/Karma)
Run all tests at once:
```
npm test
```
Run unit tests:
```
npm run test:unit
```
Run unit tests in watch mode:
```
npm run test:unit:watch
```
Run functional (integration) tests:
```
npm run test:func
```
@@ -412,4 +433,4 @@ Click [here](/docs/design.md) for details.
### Test Status
[![Sauce Test Status](https://saucelabs.com/browser-matrix/robwalch.svg)](https://saucelabs.com/u/robwalch)
[![Testing Powered By SauceLabs](https://opensource.saucelabs.com/images/opensauce/powered-by-saucelabs-badge-gray.png?sanitize=true "Testing Powered By SauceLabs")](https://saucelabs.com)
[![Testing Powered By SauceLabs](https://opensource.saucelabs.com/images/opensauce/powered-by-saucelabs-badge-gray.png?sanitize=true 'Testing Powered By SauceLabs')](https://saucelabs.com)
+2 -10
View File
@@ -4,15 +4,7 @@
"license": "Apache-2.0",
"description": "Media Source Extension - HLS library, by/for Dailymotion",
"homepage": "https://github.com/video-dev/hls.js",
"authors": [
"Guillaume du Pontavice <guillaume.dupontavice@dailymotion.com>"
],
"authors": ["Guillaume du Pontavice <guillaume.dupontavice@dailymotion.com>"],
"main": "dist/hls.js",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
]
"ignore": ["**/.*", "node_modules", "bower_components", "test", "tests"]
}
+28 -32
View File
@@ -1,42 +1,38 @@
<html>
<head>
<title>Hls.js demo - basic usage</title>
</head>
<body>
<script src="../dist/hls.js"></script>
<script src="../dist/hls.js"></script>
<center>
<h1>Hls.js demo - basic usage</h1>
<video height="600" id="video" controls></video>
</center>
<script>
if(Hls.isSupported()) {
var video = document.getElementById('video');
var hls = new Hls({
debug: true
});
hls.loadSource('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, function() {
video.muted = true;
video.play();
});
}
// hls.js is not supported on platforms that do not have Media Source Extensions (MSE) enabled.
// When the browser has built-in HLS support (check using `canPlayType`), we can provide an HLS manifest (i.e. .m3u8 URL) directly to the video element throught the `src` property.
// This is using the built-in support of the plain video element, without using hls.js.
else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
video.addEventListener('canplay',function() {
video.play();
});
}
</script>
<center>
<h1>Hls.js demo - basic usage</h1>
<video height="600" id="video" controls></video>
</center>
<script>
if (Hls.isSupported()) {
var video = document.getElementById('video');
var hls = new Hls({
debug: true,
});
hls.loadSource('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
video.muted = true;
video.play();
});
}
// hls.js is not supported on platforms that do not have Media Source Extensions (MSE) enabled.
// When the browser has built-in HLS support (check using `canPlayType`), we can provide an HLS manifest (i.e. .m3u8 URL) directly to the video element throught the `src` property.
// This is using the built-in support of the plain video element, without using hls.js.
else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
video.addEventListener('canplay', function () {
video.play();
});
}
</script>
</body>
</html>
+33 -34
View File
@@ -1,57 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<head>
<meta charset="UTF-8" />
<title></title>
</head>
<body>
<script src="../dist/hls.js"></script>
<video id="video" controls></video>
<script>
function parseQuery(queryString) {
</head>
<body>
<script src="../dist/hls.js"></script>
<video id="video" controls></video>
<script>
function parseQuery(queryString) {
var query = {};
var pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split('=');
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
var pair = pairs[i].split('=');
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
}
return query;
}
}
/* get stream from query string */
function getParameterByName(name) {
/* get stream from query string */
function getParameterByName(name) {
var query = parseQuery(window.location.search);
return query.hasOwnProperty(name) ? query[name] : undefined;
}
}
var stream = getParameterByName('stream') || 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
</script>
<script>
if(Hls.isSupported()) {
var stream = getParameterByName('stream') || 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
</script>
<script>
if (Hls.isSupported()) {
var video = document.getElementById('video');
var hls = new Hls();
hls.loadSource(stream);
hls.attachMedia(video);
hls.on(Hls.Events.MEDIA_ATTACHED, function() {
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
video.muted = true;
video.play();
});
}
</script>
<script>
var video = document.getElementById('video');
window.onload = function(){
var i=0;
}
</script>
<script>
var video = document.getElementById('video');
window.onload = function () {
var i = 0;
var el = document.getElementById('update');
function foo(){
i++;
el.innerHTML = 'animation:' + i+',decoded:' + video.webkitDecodedFrameCount + ',dropped:' + video.webkitDroppedFrameCount;
window.requestAnimationFrame(foo);
function foo() {
i++;
el.innerHTML = 'animation:' + i + ',decoded:' + video.webkitDecodedFrameCount + ',dropped:' + video.webkitDroppedFrameCount;
window.requestAnimationFrame(foo);
}
foo();
};
</script>
<div id="update"></div>
</body>
};
</script>
<div id="update"></div>
</body>
</html>
+67 -39
View File
@@ -3,7 +3,7 @@
var eventLeftMargin = 180;
var eventRightMargin = 0;
function canvasLoadEventUpdate (canvas, minTime, maxTime, events) {
function canvasLoadEventUpdate(canvas, minTime, maxTime, events) {
var event;
var start;
var ctx = canvas.getContext('2d');
@@ -11,7 +11,7 @@ function canvasLoadEventUpdate (canvas, minTime, maxTime, events) {
event = events[i];
start = event.time;
// var end = event.time + event.duration + event.latency;
if ((start >= minTime && start <= maxTime)) {
if (start >= minTime && start <= maxTime) {
y_offset += 20;
}
}
@@ -66,14 +66,14 @@ function canvasLoadEventUpdate (canvas, minTime, maxTime, events) {
event = events[i];
start = Math.round(event.time);
// var end = Math.round(event.time + event.duration + event.latency);
if ((start >= minTime && start <= maxTime)) {
if (start >= minTime && start <= maxTime) {
canvasDrawLoadEvent(ctx, y_offset, event, minTime, maxTime);
y_offset += 20;
}
}
}
function canvasVideoEventUpdate (canvas, minTime, maxTime, events) {
function canvasVideoEventUpdate(canvas, minTime, maxTime, events) {
var event;
var start;
var ctx = canvas.getContext('2d');
@@ -81,7 +81,7 @@ function canvasVideoEventUpdate (canvas, minTime, maxTime, events) {
event = events[i];
start = event.time;
// end = event.time;
if ((start >= minTime && start <= maxTime)) {
if (start >= minTime && start <= maxTime) {
y_offset += 20;
}
}
@@ -114,16 +114,19 @@ function canvasVideoEventUpdate (canvas, minTime, maxTime, events) {
event = events[i];
start = Math.round(event.time);
// end = Math.round(event.time);
if ((start >= minTime && start <= maxTime)) {
if (start >= minTime && start <= maxTime) {
canvasDrawVideoEvent(ctx, y_offset, event, minTime, maxTime);
y_offset += 20;
}
}
}
function canvasBufferWindowUpdate (canvas, minTime, maxTime, focusTime, events) {
function canvasBufferWindowUpdate(canvas, minTime, maxTime, focusTime, events) {
var ctx = canvas.getContext('2d');
var minTimeBuffer; var minTimePos; var focusTimeBuffer; var focusTimePos;
var minTimeBuffer;
var minTimePos;
var focusTimeBuffer;
var focusTimePos;
var bufferChartStart = eventLeftMargin;
var bufferChartWidth = ctx.canvas.width - eventLeftMargin - eventRightMargin;
ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -142,7 +145,10 @@ function canvasBufferWindowUpdate (canvas, minTime, maxTime, focusTime, events)
var y_offset = 0;
ctx.font = '15px Arial';
var maxBuffer = 0; var firstEventIdx = -1; var focusEventIdx = -1; var event;
var maxBuffer = 0;
var firstEventIdx = -1;
var focusEventIdx = -1;
var event;
for (var i = 0; i < events.length; i++) {
event = events[i];
maxBuffer = Math.max(maxBuffer, event.buffer + event.pos);
@@ -154,18 +160,28 @@ function canvasBufferWindowUpdate (canvas, minTime, maxTime, focusTime, events)
}
}
// compute position and buffer length at pos minTime using linear approximation
if ((firstEventIdx + 1) < events.length) {
minTimePos = events[firstEventIdx].pos + (minTime - events[firstEventIdx].time) * (events[firstEventIdx + 1].pos - events[firstEventIdx].pos) / (events[firstEventIdx + 1].time - events[firstEventIdx].time);
minTimeBuffer = minTimePos + events[firstEventIdx].buffer + (minTime - events[firstEventIdx].time) * (events[firstEventIdx + 1].buffer - events[firstEventIdx].buffer) / (events[firstEventIdx + 1].time - events[firstEventIdx].time);
if (firstEventIdx + 1 < events.length) {
minTimePos =
events[firstEventIdx].pos +
((minTime - events[firstEventIdx].time) * (events[firstEventIdx + 1].pos - events[firstEventIdx].pos)) / (events[firstEventIdx + 1].time - events[firstEventIdx].time);
minTimeBuffer =
minTimePos +
events[firstEventIdx].buffer +
((minTime - events[firstEventIdx].time) * (events[firstEventIdx + 1].buffer - events[firstEventIdx].buffer)) / (events[firstEventIdx + 1].time - events[firstEventIdx].time);
} else {
minTimeBuffer = 0;
minTimePos = 0;
}
// compute position and buffer length at pos focusTime using linear approximation
if ((focusEventIdx + 1) < events.length) {
focusTimePos = events[focusEventIdx].pos + (focusTime - events[focusEventIdx].time) * (events[focusEventIdx + 1].pos - events[focusEventIdx].pos) / (events[focusEventIdx + 1].time - events[focusEventIdx].time);
focusTimeBuffer = events[focusEventIdx].buffer + (focusTime - events[focusEventIdx].time) * (events[focusEventIdx + 1].buffer - events[focusEventIdx].buffer) / (events[focusEventIdx + 1].time - events[focusEventIdx].time);
if (focusEventIdx + 1 < events.length) {
focusTimePos =
events[focusEventIdx].pos +
((focusTime - events[focusEventIdx].time) * (events[focusEventIdx + 1].pos - events[focusEventIdx].pos)) / (events[focusEventIdx + 1].time - events[focusEventIdx].time);
focusTimeBuffer =
events[focusEventIdx].buffer +
((focusTime - events[focusEventIdx].time) * (events[focusEventIdx + 1].buffer - events[focusEventIdx].buffer)) /
(events[focusEventIdx + 1].time - events[focusEventIdx].time);
} else {
focusTimePos = 0;
focusTimeBuffer = 0;
@@ -214,7 +230,7 @@ function canvasBufferWindowUpdate (canvas, minTime, maxTime, focusTime, events)
for (var k = firstEventIdx + 1; k < events.length; k++) {
event = events[k];
x_offset = bufferChartStart + (bufferChartWidth * (event.time - minTime)) / (maxTime - minTime);
y_offset = ctx.canvas.height * (1 - (event.pos) / maxBuffer);
y_offset = ctx.canvas.height * (1 - event.pos / maxBuffer);
ctx.lineTo(x_offset, y_offset);
}
ctx.lineTo(x_offset, canvas.height);
@@ -235,7 +251,7 @@ function canvasBufferWindowUpdate (canvas, minTime, maxTime, focusTime, events)
ctx.stroke();
}
function canvasBufferTimeRangeUpdate (canvas, minTime, maxTime, windowMinTime, windowMaxTime, events) {
function canvasBufferTimeRangeUpdate(canvas, minTime, maxTime, windowMinTime, windowMaxTime, events) {
var ctx = canvas.getContext('2d');
var bufferChartStart = eventLeftMargin;
var bufferChartWidth = ctx.canvas.width - eventLeftMargin - eventRightMargin;
@@ -311,21 +327,27 @@ function canvasBufferTimeRangeUpdate (canvas, minTime, maxTime, windowMinTime, w
ctx.globalAlpha = 0.7;
ctx.fillStyle = 'grey';
var x_start = bufferChartStart;
var x_w = bufferChartWidth * (windowMinTime - minTime) / (maxTime - minTime);
var x_w = (bufferChartWidth * (windowMinTime - minTime)) / (maxTime - minTime);
ctx.fillRect(x_start, 0, x_w, canvas.height);
x_start = bufferChartStart + bufferChartWidth * (windowMaxTime - minTime) / (maxTime - minTime);
x_start = bufferChartStart + (bufferChartWidth * (windowMaxTime - minTime)) / (maxTime - minTime);
x_w = canvas.width - x_start - eventRightMargin;
ctx.fillRect(x_start, 0, x_w, canvas.height);
ctx.globalAlpha = 1;
}
function canvasBitrateEventUpdate (canvas, minTime, maxTime, windowMinTime, windowMaxTime, levelEvents, bitrateEvents) {
function canvasBitrateEventUpdate(canvas, minTime, maxTime, windowMinTime, windowMaxTime, levelEvents, bitrateEvents) {
var ctx = canvas.getContext('2d');
var bufferChartStart = eventLeftMargin;
var bufferChartWidth = ctx.canvas.width - eventLeftMargin - eventRightMargin;
var x_offset = 0;
var y_offset = 0;
var event; var maxLevel; var minLevel; var sumLevel; var maxBitrate; var minBitrate; var sumDuration;
var event;
var maxLevel;
var minLevel;
var sumLevel;
var maxBitrate;
var minBitrate;
var sumDuration;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (levelEvents.length === 0 || bitrateEvents.length === 0) {
@@ -414,16 +436,19 @@ function canvasBitrateEventUpdate (canvas, minTime, maxTime, windowMinTime, wind
ctx.globalAlpha = 0.7;
ctx.fillStyle = 'grey';
var x_start = bufferChartStart;
var x_w = bufferChartWidth * (windowMinTime - minTime) / (maxTime - minTime);
var x_w = (bufferChartWidth * (windowMinTime - minTime)) / (maxTime - minTime);
ctx.fillRect(x_start, 0, x_w, canvas.height);
x_start = bufferChartStart + bufferChartWidth * (windowMaxTime - minTime) / (maxTime - minTime);
x_start = bufferChartStart + (bufferChartWidth * (windowMaxTime - minTime)) / (maxTime - minTime);
x_w = canvas.width - x_start - eventRightMargin;
ctx.fillRect(x_start, 0, x_w, canvas.height);
ctx.globalAlpha = 1;
}
function canvasDrawLoadEvent (ctx, yoffset, event, minTime, maxTime) {
var legend; var offset; var x_start; var x_w;
function canvasDrawLoadEvent(ctx, yoffset, event, minTime, maxTime) {
var legend;
var offset;
var x_start;
var x_w;
var networkChartStart = eventLeftMargin;
var networkChartWidth = ctx.canvas.width - eventLeftMargin - eventRightMargin;
var tend = Math.round(event.time + event.duration + event.latency);
@@ -433,32 +458,32 @@ function canvasDrawLoadEvent (ctx, yoffset, event, minTime, maxTime) {
ctx.font = '12px Arial';
legend = Math.round(event.time);
offset = ctx.measureText(legend).width + 5;
x_start = networkChartStart - offset + networkChartWidth * (event.time - minTime) / (maxTime - minTime);
x_start = networkChartStart - offset + (networkChartWidth * (event.time - minTime)) / (maxTime - minTime);
ctx.fillText(legend, x_start, yoffset + 12);
// draw latency rectangle
ctx.fillStyle = 'orange';
x_start = networkChartStart + networkChartWidth * (event.time - minTime) / (maxTime - minTime);
x_w = networkChartWidth * event.latency / (maxTime - minTime);
x_start = networkChartStart + (networkChartWidth * (event.time - minTime)) / (maxTime - minTime);
x_w = (networkChartWidth * event.latency) / (maxTime - minTime);
ctx.fillRect(x_start, yoffset, x_w, 15);
// draw download rectangle
ctx.fillStyle = 'green';
x_start = networkChartStart + networkChartWidth * (event.time + event.latency - minTime) / (maxTime - minTime);
x_w = networkChartWidth * event.load / (maxTime - minTime);
x_start = networkChartStart + (networkChartWidth * (event.time + event.latency - minTime)) / (maxTime - minTime);
x_w = (networkChartWidth * event.load) / (maxTime - minTime);
ctx.fillRect(x_start, yoffset, x_w, 15);
if (event.parsing) {
// draw parsing rectangle
ctx.fillStyle = 'blue';
x_start = networkChartStart + networkChartWidth * (event.time + event.latency + event.load - minTime) / (maxTime - minTime);
x_w = networkChartWidth * event.parsing / (maxTime - minTime);
x_start = networkChartStart + (networkChartWidth * (event.time + event.latency + event.load - minTime)) / (maxTime - minTime);
x_w = (networkChartWidth * event.parsing) / (maxTime - minTime);
ctx.fillRect(x_start, yoffset, x_w, 15);
if (event.buffer) {
// draw buffering rectangle
ctx.fillStyle = 'red';
x_start = networkChartStart + networkChartWidth * (event.time + event.latency + event.load + event.parsing - minTime) / (maxTime - minTime);
x_w = networkChartWidth * event.buffer / (maxTime - minTime);
x_start = networkChartStart + (networkChartWidth * (event.time + event.latency + event.load + event.parsing - minTime)) / (maxTime - minTime);
x_w = (networkChartWidth * event.buffer) / (maxTime - minTime);
ctx.fillRect(x_start, yoffset, x_w, 15);
}
}
@@ -545,8 +570,11 @@ function canvasDrawLoadEvent (ctx, yoffset, event, minTime, maxTime) {
ctx.fillText(legend, 5, yoffset + 15);
}
function canvasDrawVideoEvent (ctx, yoffset, event, minTime, maxTime) {
var legend; var offset; var x_start; var x_w;
function canvasDrawVideoEvent(ctx, yoffset, event, minTime, maxTime) {
var legend;
var offset;
var x_start;
var x_w;
var networkChartStart = eventLeftMargin;
var networkChartWidth = ctx.canvas.width - eventLeftMargin - eventRightMargin;
@@ -564,13 +592,13 @@ function canvasDrawVideoEvent (ctx, yoffset, event, minTime, maxTime) {
ctx.font = '12px Arial';
legend = Math.round(event.time);
offset = ctx.measureText(legend).width + 5;
x_start = networkChartStart - offset + networkChartWidth * (event.time - minTime) / (maxTime - minTime);
x_start = networkChartStart - offset + (networkChartWidth * (event.time - minTime)) / (maxTime - minTime);
ctx.fillText(legend, x_start, yoffset + 12);
// draw event rectangle
x_start = networkChartStart + networkChartWidth * (event.time - minTime) / (maxTime - minTime);
x_start = networkChartStart + (networkChartWidth * (event.time - minTime)) / (maxTime - minTime);
if (event.duration) {
x_w = networkChartWidth * event.duration / (maxTime - minTime);
x_w = (networkChartWidth * event.duration) / (maxTime - minTime);
} else {
x_w = 1;
}
+15 -16
View File
@@ -21,19 +21,19 @@ Chart.controllers.horizontalBar.prototype.calculateBarValuePixels = function (da
size: size,
base: base,
head: head,
center: head + size / 2
center: head + size / 2,
};
};
Chart.controllers.horizontalBar.prototype.calculateBarIndexPixels = function (datasetIndex, index, ruler, options) {
const rowHeight = options.barThickness;
const size = rowHeight * options.categoryPercentage;
const center = ruler.start + (datasetIndex * rowHeight + (rowHeight / 2));
const center = ruler.start + (datasetIndex * rowHeight + rowHeight / 2);
return {
base: center - size / 2,
head: center + size / 2,
center,
size
size,
};
};
@@ -49,7 +49,7 @@ Chart.controllers.horizontalBar.prototype.draw = function () {
const scale = this._getValueScale();
scale._parseValue = scaleParseValue;
const ctx: CanvasRenderingContext2D = chart.ctx;
const chartArea: { left, top, right, bottom } = chart.chartArea;
const chartArea: { left; top; right; bottom } = chart.chartArea;
Chart.helpers.canvas.clipArea(ctx, chartArea);
if (!this.lineHeight) {
this.lineHeight = Math.ceil(ctx.measureText('0').actualBoundingBoxAscent) + 2;
@@ -73,7 +73,7 @@ Chart.controllers.horizontalBar.prototype.draw = function () {
const isFragment = dataType === 'fragment' || isPart || isFragmentHint;
const isCue = dataType === 'cue';
if (isCue) {
view.y += (view.height * 0.5 * (i % 2)) - (view.height * 0.25);
view.y += view.height * 0.5 * (i % 2) - view.height * 0.25;
} else if (isPart) {
view.height -= 22;
}
@@ -113,7 +113,7 @@ Chart.controllers.horizontalBar.prototype.draw = function () {
}
if (stats.loaded && stats.total) {
ctx.fillStyle = 'rgba(50, 20, 100, 0.3)';
ctx.fillRect(bounds.x, bounds.y, bounds.w * stats.loaded / stats.total, bounds.h);
ctx.fillRect(bounds.x, bounds.y, (bounds.w * stats.loaded) / stats.total, bounds.h);
}
} else if (isCue) {
if (obj.active) {
@@ -155,14 +155,14 @@ Chart.controllers.horizontalBar.prototype.draw = function () {
Chart.helpers.canvas.unclipArea(chart.ctx);
};
export function applyChartInstanceOverrides (chart) {
export function applyChartInstanceOverrides(chart) {
Object.keys(chart.scales).forEach((axis) => {
const scale = chart.scales![axis];
scale._parseValue = scaleParseValue;
});
}
function scaleParseValue (value: number[] | any) {
function scaleParseValue(value: number[] | any) {
if (value === undefined) {
console.warn('Chart values undefined (update chart)');
return {};
@@ -193,15 +193,15 @@ function scaleParseValue (value: number[] | any) {
min,
max,
start,
end
end,
};
}
function intersects (x1, x2, x3, x4) {
function intersects(x1, x2, x3, x4) {
return x2 > x3 && x1 < x4;
}
function boundingRects (vm) {
function boundingRects(vm) {
const half = vm.height / 2;
const left = Math.min(vm.x, vm.base);
const right = Math.max(vm.x, vm.base);
@@ -211,19 +211,18 @@ function boundingRects (vm) {
x: left,
y: top,
w: right - left,
h: bottom - top
h: bottom - top,
};
}
export function hhmmss (value, fixedDigits) {
export function hhmmss(value, fixedDigits) {
const h = (value / 3600) | 0;
const m = ((value / 60) | 0) % 60;
const s = value % 60;
return `${h}:${pad(m, 2)}:${pad(s.toFixed(fixedDigits), fixedDigits ? (fixedDigits + 3) : 2)}`
.replace(/^(?:0+:?)*(\d.*?)(?:\.0*)?$/, '$1');
return `${h}:${pad(m, 2)}:${pad(s.toFixed(fixedDigits), fixedDigits ? fixedDigits + 3 : 2)}`.replace(/^(?:0+:?)*(\d.*?)(?:\.0*)?$/, '$1');
}
function pad (str, length) {
function pad(str, length) {
str = '' + str;
while (str.length < length) {
str = '0' + str;
+198 -159
View File
@@ -19,16 +19,16 @@ declare global {
const X_AXIS_SECONDS = 'x-axis-seconds';
interface ChartScale {
height: number,
min: number,
max: number,
options : any,
determineDataLimits: () => void,
buildTicks: () => void,
getLabelForIndex: (index: number, datasetIndex: number) => string,
getPixelForTick: (index: number) => number,
getPixelForValue: (value: number, index?: number, datasetIndex?: number) => number,
getValueForPixel: (pixel: number) => number
height: number;
min: number;
max: number;
options: any;
determineDataLimits: () => void;
buildTicks: () => void;
getLabelForIndex: (index: number, datasetIndex: number) => string;
getPixelForTick: (index: number) => number;
getPixelForValue: (value: number, index?: number, datasetIndex?: number) => number;
getValueForPixel: (pixel: number) => number;
}
export class TimelineChart {
@@ -40,25 +40,27 @@ export class TimelineChart {
private cuesChangeHandler?: (e) => void;
private hidden: boolean = true;
constructor (canvas: HTMLCanvasElement, chartJsOptions?: any) {
constructor(canvas: HTMLCanvasElement, chartJsOptions?: any) {
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error(`Could not get CanvasRenderingContext2D from canvas: ${canvas}`);
}
const chart = this.chart = self.chart = new Chart(ctx, {
const chart = (this.chart = self.chart = new Chart(ctx, {
type: 'horizontalBar',
data: {
labels: [],
datasets: []
datasets: [],
},
options: Object.assign(getChartOptions(), chartJsOptions),
plugins: [{
afterRender: () => {
this.imageDataBuffer = null;
this.drawCurrentTime();
}
}]
});
plugins: [
{
afterRender: () => {
this.imageDataBuffer = null;
this.drawCurrentTime();
},
},
],
}));
applyChartInstanceOverrides(chart);
@@ -81,7 +83,7 @@ export class TimelineChart {
canvas.ondblclick = (event: MouseEvent) => {
const chart = this.chart;
const chartArea: { left, top, right, bottom } = chart.chartArea;
const chartArea: { left; top; right; bottom } = chart.chartArea;
const element = chart.getElementAtEvent(event);
const pos = Chart.helpers.getRelativePosition(event, chart);
const scale = this.chartScales[X_AXIS_SECONDS];
@@ -106,11 +108,11 @@ export class TimelineChart {
// TODO: Prevent zoom over y axis labels
}
get chartScales (): { 'x-axis-seconds' : ChartScale } {
get chartScales(): { 'x-axis-seconds': ChartScale } {
return (this.chart as any).scales;
}
reset () {
reset() {
const scale = this.chartScales[X_AXIS_SECONDS];
scale.options.ticks.min = 0;
scale.options.ticks.max = 60;
@@ -126,17 +128,17 @@ export class TimelineChart {
}
}
update () {
update() {
if (this.hidden || !this.chart.ctx?.canvas.width) {
return;
}
this.chart.update({
duration: 0,
lazy: true
lazy: true,
});
}
resize (datasets?) {
resize(datasets?) {
if (this.hidden) {
return;
}
@@ -155,15 +157,15 @@ export class TimelineChart {
});
}
show () {
show() {
this.hidden = false;
}
hide () {
hide() {
this.hidden = true;
}
updateLevels (levels: Level[], levelSwitched) {
updateLevels(levels: Level[], levelSwitched) {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
@@ -184,12 +186,14 @@ export class TimelineChart {
} else if (nextAutoLevel === i) {
borderColor = 'rgba(160, 0, 160, 1.0)';
}
datasets.push(datasetWithDefaults({
url: Array.isArray(level.url) ? level.url[level.urlId || 0] : level.url,
trackType: 'level',
borderColor,
level: index
}));
datasets.push(
datasetWithDefaults({
url: Array.isArray(level.url) ? level.url[level.urlId || 0] : level.url,
trackType: 'level',
borderColor,
level: index,
})
);
if (level.details) {
this.updateLevelOrTrack(level.details);
}
@@ -197,7 +201,7 @@ export class TimelineChart {
this.resize(datasets);
}
updateAudioTracks (audioTracks: MediaPlaylist[]) {
updateAudioTracks(audioTracks: MediaPlaylist[]) {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
@@ -205,12 +209,14 @@ export class TimelineChart {
const { audioTrack } = self.hls;
audioTracks.forEach((track: MediaPlaylist, i) => {
labels.push(getAudioTrackName(track, i));
datasets.push(datasetWithDefaults({
url: track.url,
trackType: 'audioTrack',
borderColor: audioTrack === i ? 'rgba(32, 32, 240, 1.0)' : null,
audioTrack: i
}));
datasets.push(
datasetWithDefaults({
url: track.url,
trackType: 'audioTrack',
borderColor: audioTrack === i ? 'rgba(32, 32, 240, 1.0)' : null,
audioTrack: i,
})
);
if (track.details) {
this.updateLevelOrTrack(track.details);
}
@@ -218,7 +224,7 @@ export class TimelineChart {
this.resize(datasets);
}
updateSubtitleTracks (subtitles: MediaPlaylist[]) {
updateSubtitleTracks(subtitles: MediaPlaylist[]) {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
@@ -226,12 +232,14 @@ export class TimelineChart {
const { subtitleTrack } = self.hls;
subtitles.forEach((track, i) => {
labels.push(getSubtitlesName(track, i));
datasets.push(datasetWithDefaults({
url: track.url,
trackType: 'subtitleTrack',
borderColor: subtitleTrack === i ? 'rgba(32, 32, 240, 1.0)' : null,
subtitleTrack: i
}));
datasets.push(
datasetWithDefaults({
url: track.url,
trackType: 'subtitleTrack',
borderColor: subtitleTrack === i ? 'rgba(32, 32, 240, 1.0)' : null,
subtitleTrack: i,
})
);
if (track.details) {
this.updateLevelOrTrack(track.details);
}
@@ -239,7 +247,7 @@ export class TimelineChart {
this.resize(datasets);
}
removeType (trackType: 'level' | 'audioTrack' | 'subtitleTrack' | 'textTrack') {
removeType(trackType: 'level' | 'audioTrack' | 'subtitleTrack' | 'textTrack') {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
@@ -253,13 +261,12 @@ export class TimelineChart {
}
}
updateLevelOrTrack (details: LevelDetails) {
updateLevelOrTrack(details: LevelDetails) {
const { targetduration, totalduration, url } = details;
const { datasets } = this.chart.data;
// eslint-disable-next-line no-restricted-properties
const deliveryDirectivePattern = /[?&]_HLS_(?:msn|part|skip)=[^?&]+/g;
const levelDataSet = arrayFind(datasets, dataset =>
dataset.url?.toString().replace(deliveryDirectivePattern, '') === url.replace(deliveryDirectivePattern, ''));
const levelDataSet = arrayFind(datasets, (dataset) => dataset.url?.toString().replace(deliveryDirectivePattern, '') === url.replace(deliveryDirectivePattern, ''));
if (!levelDataSet) {
return;
}
@@ -269,22 +276,37 @@ export class TimelineChart {
details.fragments.forEach((fragment) => {
// TODO: keep track of initial playlist start and duration so that we can show drift and pts offset
// (Make that a feature of hls.js v1.0.0 fragments)
data.push(Object.assign({
dataType: 'fragment'
}, fragment));
data.push(
Object.assign(
{
dataType: 'fragment',
},
fragment
)
);
});
}
if (details.partList) {
details.partList.forEach((part) => {
data.push(Object.assign({
dataType: 'part',
start: part.fragment.start + part.fragOffset
}, part));
data.push(
Object.assign(
{
dataType: 'part',
start: part.fragment.start + part.fragOffset,
},
part
)
);
});
if (details.fragmentHint) {
data.push(Object.assign({
dataType: 'fragmentHint'
}, details.fragmentHint));
data.push(
Object.assign(
{
dataType: 'fragmentHint',
},
details.fragmentHint
)
);
}
}
const start = getPlaylistStart(details);
@@ -296,7 +318,7 @@ export class TimelineChart {
}
// @ts-ignore
get minZoom (): number {
get minZoom(): number {
if (this.chart.config?.options?.plugins) {
return this.chart.config.options.plugins.zoom.zoom.rangeMin.x;
}
@@ -304,7 +326,7 @@ export class TimelineChart {
}
// @ts-ignore
get maxZoom (): number {
get maxZoom(): number {
if (this.chart.config?.options?.plugins) {
return this.chart.config.options.plugins.zoom.zoom.rangeMax.x;
}
@@ -312,7 +334,7 @@ export class TimelineChart {
}
// @ts-ignore
set maxZoom (x: number) {
set maxZoom(x: number) {
const { chart } = this;
const { config } = chart;
if (config?.options?.plugins) {
@@ -326,15 +348,15 @@ export class TimelineChart {
}
}
updateFragment (data: FragLoadedData | FragParsedData | FragChangedData) {
updateFragment(data: FragLoadedData | FragParsedData | FragChangedData) {
const { datasets } = this.chart.data;
const frag: Fragment = data.frag;
const levelDataSet = arrayFind(datasets, dataset => dataset.url === frag.baseurl);
const levelDataSet = arrayFind(datasets, (dataset) => dataset.url === frag.baseurl);
if (!levelDataSet) {
return;
}
// eslint-disable-next-line no-restricted-properties
const fragData = arrayFind(levelDataSet.data, fragData => fragData.relurl === frag.relurl && fragData.sn === frag.sn);
const fragData = arrayFind(levelDataSet.data, (fragData) => fragData.relurl === frag.relurl && fragData.sn === frag.sn);
if (fragData && fragData !== frag) {
Object.assign(fragData, frag);
}
@@ -344,12 +366,12 @@ export class TimelineChart {
this.rafDebounceRequestId = self.requestAnimationFrame(() => this.update());
}
updateSourceBuffers (tracks: TrackSet, media: HTMLMediaElement) {
updateSourceBuffers(tracks: TrackSet, media: HTMLMediaElement) {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
}
const trackTypes = Object.keys(tracks).sort((type) => type === 'video' ? 1 : -1);
const trackTypes = Object.keys(tracks).sort((type) => (type === 'video' ? 1 : -1));
const mediaBufferData = [];
this.removeSourceBuffers();
@@ -363,15 +385,17 @@ export class TimelineChart {
const backgroundColor = {
video: 'rgba(0, 0, 255, 0.2)',
audio: 'rgba(128, 128, 0, 0.2)',
audiovideo: 'rgba(128, 128, 255, 0.2)'
audiovideo: 'rgba(128, 128, 255, 0.2)',
}[type];
labels.unshift(`${type} buffer (${track.id})`);
datasets.unshift(datasetWithDefaults({
data,
categoryPercentage: 0.5,
backgroundColor,
sourceBuffer
}));
datasets.unshift(
datasetWithDefaults({
data,
categoryPercentage: 0.5,
backgroundColor,
sourceBuffer,
})
);
sourceBuffer.onupdate = () => {
try {
replaceTimeRangeTuples(sourceBuffer.buffered, data);
@@ -393,12 +417,14 @@ export class TimelineChart {
}
labels.unshift('media buffer');
datasets.unshift(datasetWithDefaults({
data: mediaBufferData,
categoryPercentage: 0.5,
backgroundColor: 'rgba(0, 255, 0, 0.2)',
media
}));
datasets.unshift(
datasetWithDefaults({
data: mediaBufferData,
categoryPercentage: 0.5,
backgroundColor: 'rgba(0, 255, 0, 0.2)',
media,
})
);
media.ontimeupdate = () => this.drawCurrentTime();
@@ -415,7 +441,7 @@ export class TimelineChart {
this.resize(datasets);
}
removeSourceBuffers () {
removeSourceBuffers() {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
@@ -429,7 +455,7 @@ export class TimelineChart {
}
}
setTextTracks (textTracks) {
setTextTracks(textTracks) {
const { labels, datasets } = this.chart.data;
if (!labels || !datasets) {
return;
@@ -442,14 +468,16 @@ export class TimelineChart {
// }
const data = [];
labels.push(`${textTrack.name || textTrack.label} ${textTrack.kind} (${textTrack.mode})`);
datasets.push(datasetWithDefaults({
data,
categoryPercentage: 0.5,
url: '',
trackType: 'textTrack',
borderColor: textTrack.mode !== 'hidden' === i ? 'rgba(32, 32, 240, 1.0)' : null,
textTrack: i
}));
datasets.push(
datasetWithDefaults({
data,
categoryPercentage: 0.5,
url: '',
trackType: 'textTrack',
borderColor: (textTrack.mode !== 'hidden') === i ? 'rgba(32, 32, 240, 1.0)' : null,
textTrack: i,
})
);
this.cuesChangeHandler = this.cuesChangeHandler || ((e) => this.updateTextTrackCues(e.currentTarget));
textTrack._data = data;
textTrack.removeEventListener('cuechange', this.cuesChangeHandler);
@@ -459,7 +487,7 @@ export class TimelineChart {
this.resize(datasets);
}
updateTextTrackCues (textTrack) {
updateTextTrackCues(textTrack) {
const data = textTrack._data;
if (!data) {
return;
@@ -509,7 +537,7 @@ export class TimelineChart {
end,
content,
active,
dataType: 'cue'
dataType: 'cue',
});
}
if (this.hidden) {
@@ -521,7 +549,7 @@ export class TimelineChart {
});
}
drawCurrentTime () {
drawCurrentTime() {
const chart = this.chart;
if (self.hls && self.hls.media && chart.data.datasets!.length) {
const currentTime = self.hls.media.currentTime;
@@ -530,7 +558,7 @@ export class TimelineChart {
if (this.hidden || !ctx || !ctx.canvas.width) {
return;
}
const chartArea: { left, top, right, bottom } = chart.chartArea;
const chartArea: { left; top; right; bottom } = chart.chartArea;
const x = scale.getPixelForValue(currentTime);
ctx.restore();
ctx.save();
@@ -544,7 +572,7 @@ export class TimelineChart {
}
}
getCurrentTimeColor (video: HTMLMediaElement): string {
getCurrentTimeColor(video: HTMLMediaElement): string {
if (!video.readyState || video.ended) {
return 'rgba(0, 0, 0, 0.9)';
}
@@ -557,7 +585,7 @@ export class TimelineChart {
return 'rgba(0, 0, 255, 0.9)';
}
drawLineX (ctx, x: number, chartArea) {
drawLineX(ctx, x: number, chartArea) {
if (!this.imageDataBuffer) {
const devicePixelRatio = self.devicePixelRatio || 1;
this.imageDataBuffer = ctx.getImageData(0, 0, chartArea.right * devicePixelRatio, chartArea.bottom * devicePixelRatio);
@@ -577,20 +605,23 @@ export class TimelineChart {
}
}
function datasetWithDefaults (options) {
return Object.assign({
data: [],
xAxisID: X_AXIS_SECONDS,
barThickness: 35,
categoryPercentage: 1
}, options);
function datasetWithDefaults(options) {
return Object.assign(
{
data: [],
xAxisID: X_AXIS_SECONDS,
barThickness: 35,
categoryPercentage: 1,
},
options
);
}
function getPlaylistStart (details: LevelDetails): number {
return (details.fragments && details.fragments.length) ? details.fragments[0].start : 0;
function getPlaylistStart(details: LevelDetails): number {
return details.fragments && details.fragments.length ? details.fragments[0].start : 0;
}
function getLevelName (level: Level, index: number) {
function getLevelName(level: Level, index: number) {
let label = '(main playlist)';
if (level.attrs && level.attrs.BANDWIDTH) {
label = `${getMainLevelAttribute(level)}@${level.attrs.BANDWIDTH}`;
@@ -603,21 +634,21 @@ function getLevelName (level: Level, index: number) {
return `${label} L-${index}`;
}
function getMainLevelAttribute (level: Level) {
function getMainLevelAttribute(level: Level) {
return level.attrs.RESOLUTION || level.attrs.CODECS || level.attrs.AUDIO;
}
function getAudioTrackName (track: MediaPlaylist, index: number) {
function getAudioTrackName(track: MediaPlaylist, index: number) {
const label = track.lang ? `${track.name}/${track.lang}` : track.name;
return `${label} (${track.groupId || track.attrs['GROUP-ID']}) A-${index}`;
}
function getSubtitlesName (track: MediaPlaylist, index: number) {
function getSubtitlesName(track: MediaPlaylist, index: number) {
const label = track.lang ? `${track.name}/${track.lang}` : track.name;
return `${label} (${track.groupId || track.attrs['GROUP-ID']}) S-${index}`;
}
function replaceTimeRangeTuples (timeRanges, data) {
function replaceTimeRangeTuples(timeRanges, data) {
data.length = 0;
const { length } = timeRanges;
for (let i = 0; i < length; i++) {
@@ -625,15 +656,17 @@ function replaceTimeRangeTuples (timeRanges, data) {
}
}
function cuesMatch (cue1, cue2) {
return cue1.startTime === cue2.startTime &&
function cuesMatch(cue1, cue2) {
return (
cue1.startTime === cue2.startTime &&
cue1.endTime === cue2.endTime &&
cue1.text === cue2.text &&
cue1.data === cue2.data &&
JSON.stringify(cue1.value) === JSON.stringify(cue2.value);
JSON.stringify(cue1.value) === JSON.stringify(cue2.value)
);
}
function getCueLabel (cue) {
function getCueLabel(cue) {
if (cue.text) {
return cue.text;
}
@@ -641,7 +674,7 @@ function getCueLabel (cue) {
return JSON.stringify(result);
}
function parseDataCue (cue) {
function parseDataCue(cue) {
const data = {};
const { value } = cue;
if (value) {
@@ -659,55 +692,57 @@ function parseDataCue (cue) {
return data;
}
function getChartOptions () {
function getChartOptions() {
return {
animation: {
duration: 0
duration: 0,
},
elements: {
rectangle: {
borderWidth: 1,
borderColor: 'rgba(20, 20, 20, 1)'
}
borderColor: 'rgba(20, 20, 20, 1)',
},
},
events: [
'click', 'touchstart'
],
events: ['click', 'touchstart'],
hover: {
mode: null,
animationDuration: 0
animationDuration: 0,
},
legend: {
display: false
display: false,
},
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
scales: {
// TODO: additional xAxes for PTS and PDT
xAxes: [{
id: X_AXIS_SECONDS,
ticks: {
beginAtZero: true,
sampleSize: 0,
maxRotation: 0,
callback: (tickValue, i, ticks) => {
if (i === 0 || i === ticks.length - 1) {
return tickValue ? '' : '0';
} else {
return hhmmss(tickValue, 2);
}
}
}
}],
yAxes: [{
gridLines: {
display: false
}
}]
xAxes: [
{
id: X_AXIS_SECONDS,
ticks: {
beginAtZero: true,
sampleSize: 0,
maxRotation: 0,
callback: (tickValue, i, ticks) => {
if (i === 0 || i === ticks.length - 1) {
return tickValue ? '' : '0';
} else {
return hhmmss(tickValue, 2);
}
},
},
},
],
yAxes: [
{
gridLines: {
display: false,
},
},
],
},
tooltips: {
enabled: false
enabled: false,
},
plugins: {
zoom: {
@@ -715,29 +750,33 @@ function getChartOptions () {
enabled: true,
mode: 'x',
rangeMin: {
x: -10, y: null
x: -10,
y: null,
},
rangeMax: {
x: null, y: null
}
x: null,
y: null,
},
},
zoom: {
enabled: true,
speed: 0.1,
mode: 'x',
rangeMin: {
x: 0, y: null
x: 0,
y: null,
},
rangeMax: {
x: 60, y: null
}
}
}
}
x: 60,
y: null,
},
},
},
},
};
}
function arrayFind (array, predicate) {
function arrayFind(array, predicate) {
const len = array.length >>> 0;
if (typeof predicate !== 'function') {
throw TypeError('predicate must be a function');
+2 -2
View File
@@ -1,4 +1,4 @@
export function sortObject (obj) {
export function sortObject(obj) {
if (typeof obj !== 'object') {
return obj;
}
@@ -14,7 +14,7 @@ export function sortObject (obj) {
return temp;
}
export function copyTextToClipboard (text) {
export function copyTextToClipboard(text) {
let textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
+126 -112
View File
@@ -1,11 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta charset="utf-8" />
<title>hls.js demo</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap-theme.min.css" />
<link rel="stylesheet" href="style.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js"></script>
@@ -16,13 +16,11 @@
<header class="wrapper clearfix">
<h1>
<a target="_blank" href="https://github.com/video-dev/hls.js">
<img src="https://cloud.githubusercontent.com/assets/616833/19739063/e10be95a-9bb9-11e6-8100-2896f8500138.png"/>
<img src="https://cloud.githubusercontent.com/assets/616833/19739063/e10be95a-9bb9-11e6-8100-2896f8500138.png" />
</a>
</h1>
<h2 class="title">
demo
</h2>
<h2 class="title">demo</h2>
<h3>
<a href="../api-docs">API docs | usage guide</a>
@@ -32,14 +30,10 @@
<div class="main-container">
<header>
<p>Test your HLS streams in all supported browsers (Chrome/Firefox/IE11/Edge/Safari).</p>
<p>Advanced controls are available at the bottom of this page.</p>
<p>
Test your HLS streams in all supported browsers (Chrome/Firefox/IE11/Edge/Safari).
</p>
<p>
Advanced controls are available at the bottom of this page.
</p>
<p>
<b>Looking for a more <i>basic</i> usage example? Go <a href="basic-usage.html">here</a>.</b><br>
<b>Looking for a more <i>basic</i> usage example? Go <a href="basic-usage.html">here</a>.</b><br />
</p>
</header>
<div id="controls">
@@ -48,39 +42,36 @@
<option value="" selected>Select a test-stream from drop-down menu. Or enter custom URL below</option>
</select>
<input id="streamURL" class="innerControls" type=text value=""/>
<input id="streamURL" class="innerControls" type="text" value="" />
<label class="innerControls"
title="Uncheck this to disable loading of streams selected from the drop-down above.">
<label class="innerControls" title="Uncheck this to disable loading of streams selected from the drop-down above.">
Enable streaming:
<input id="enableStreaming" type=checkbox checked/>
<input id="enableStreaming" type="checkbox" checked />
</label>
<label class="innerControls"
title="When a media error occurs, attempt to recover playback by calling `hls.recoverMediaError()`.">
<label class="innerControls" title="When a media error occurs, attempt to recover playback by calling `hls.recoverMediaError()`.">
Auto-recover media-errors:
<input id="autoRecoverError" type=checkbox checked/>
<input id="autoRecoverError" type="checkbox" checked />
</label>
<label class="innerControls"
title="Stop loading and playback if playback under-buffer stalls. This can help debug stall errors.">
<label class="innerControls" title="Stop loading and playback if playback under-buffer stalls. This can help debug stall errors.">
Stop on first stall:
<input id="stopOnStall" type=checkbox unchecked/>
<input id="stopOnStall" type="checkbox" unchecked />
</label>
<label class="innerControls">
Dump transmuxed fMP4 data:
<input id="dumpfMP4" type=checkbox unchecked/>
<input id="dumpfMP4" type="checkbox" unchecked />
</label>
<label class="innerControls">
Metrics history (max limit, -1 is unlimited):
<input id="limitMetrics" style="width: 8em" type=number/>
<input id="limitMetrics" style="width: 8em" type="number" />
</label>
<label class="innerControls">
HTML video element width:
<select id="videoSize" style="float:right;">
<select id="videoSize" style="float: right">
<option value="240px">240px</option>
<option value="426px">426px</option>
<option value="640px">640px</option>
@@ -110,33 +101,30 @@
<div class="config-editor-wrapper">
<div class="config-editor-container">
<div id="config-editor">
Loading...
</div>
<div id="config-editor">Loading...</div>
</div>
<div class="config-editor-commands">
<label for="config-persistence">
Persist
<input name="config-persistence" id="config-persistence" type="checkbox">
Persist
<input name="config-persistence" id="config-persistence" type="checkbox" />
</label>
<button name="config-apply" onclick="applyConfigEditorValue()">Apply</button>
</div>
</div>
</div>
<video id="video" controls autoplay class="videoCentered" style="width: 80%;"></video>
<br>
<canvas id="bufferedCanvas" width="720" height="15" class="videoCentered" onclick="onClickBufferedRange(event);" style="height: fit-content;"></canvas>
<br>
<br>
<video id="video" controls autoplay class="videoCentered" style="width: 80%"></video>
<br />
<canvas id="bufferedCanvas" width="720" height="15" class="videoCentered" onclick="onClickBufferedRange(event);" style="height: fit-content"></canvas>
<br />
<br />
<label class="center">Status:</label>
<pre id="statusOut" class="center" style="white-space: pre-wrap;"></pre>
<pre id="statusOut" class="center" style="white-space: pre-wrap"></pre>
<label class="center">Error:</label>
<pre id="errorOut" class="center" style="white-space: pre-wrap;"></pre>
<pre id="errorOut" class="center" style="white-space: pre-wrap"></pre>
<div class="center" style="text-align: center; display: none;" id="toggleButtons">
<div class="center" style="text-align: center; display: none" id="toggleButtons">
<button type="button" class="btn btn-sm demo-tab-btn" data-tab="playbackControlTab" onclick="toggleTabClick(this);">Playback</button>
<button type="button" class="btn btn-sm demo-tab-btn" data-tab="timelineTab" onclick="toggleTabClick(this);">Timeline</button>
<button type="button" class="btn btn-sm demo-tab-btn" data-tab="qualityLevelControlTab" onclick="toggleTabClick(this);">Quality-levels</button>
@@ -145,8 +133,8 @@
<button type="button" class="btn btn-sm demo-tab-btn" data-tab="statsDisplayTab" onclick="toggleTabClick(this);">Buffer &amp; Statistics</button>
</div>
<div class="center demo-tab" id="playbackControlTab" style="display: none;">
<br>
<div class="center demo-tab" id="playbackControlTab" style="display: none">
<br />
<center>
<p>
<span>
@@ -154,20 +142,34 @@
<button type="button" class="btn btn-sm btn-info" title="video.pause()" onclick="$('#video')[0].pause()">Pause</button>
</span>
<span>
<button type="button" class="btn btn-sm btn-info" title="video.playbackRate = text input" onclick="$('#video')[0].defaultPlaybackRate=$('#video')[0].playbackRate=$('#playback_rate').val();">Playback rate </button>
<input type="number" value="1" id="playback_rate" size="8" style="width: 3em;" onkeydown="if(window.event.keyCode=='13'){$('#video')[0].defaultPlaybackRate=$('#video')[0].playbackRate=$('#playback_rate').val();}">
<button
type="button"
class="btn btn-sm btn-info"
title="video.playbackRate = text input"
onclick="$('#video')[0].defaultPlaybackRate=$('#video')[0].playbackRate=$('#playback_rate').val();"
>
Playback rate
</button>
<input
type="number"
value="1"
id="playback_rate"
size="8"
style="width: 3em"
onkeydown="if(window.event.keyCode=='13'){$('#video')[0].defaultPlaybackRate=$('#video')[0].playbackRate=$('#playback_rate').val();}"
/>
</span>
<span>
<button type="button" class="btn btn-sm btn-info" title="video.currentTime -= 10" onclick="$('#video')[0].currentTime-=10">- 10 s</button>
<button type="button" class="btn btn-sm btn-info" title="video.currentTime += 10" onclick="$('#video')[0].currentTime+=10">+ 10 s</button>
</span>
<span>
<button type="button" class="btn btn-sm btn-info" title="video.currentTime = text input" onclick="$('#video')[0].currentTime=$('#seek_pos').val();">Seek to </button>
<input type="number" id="seek_pos" size="8" style="width: 7em;" onkeydown="if(window.event.keyCode=='13'){$('#video')[0].currentTime=$('#seek_pos').val();}">
<button type="button" class="btn btn-sm btn-info" title="video.currentTime = text input" onclick="$('#video')[0].currentTime=$('#seek_pos').val();">Seek to</button>
<input type="number" id="seek_pos" size="8" style="width: 7em" onkeydown="if(window.event.keyCode=='13'){$('#video')[0].currentTime=$('#seek_pos').val();}" />
</span>
</p>
<p>
<span>
<span>
<button type="button" class="btn btn-xs btn-warning" title="hls.startLoad()" onclick="hls.startLoad()">Start loading</button>
<button type="button" class="btn btn-xs btn-warning" title="hls.stopLoad()" onclick="hls.stopLoad()">Stop loading</button>
</span>
@@ -192,66 +194,72 @@
</center>
</div>
<div class="center demo-tab demo-timeline-chart-container" id="timelineTab" style="display: none;">
<div class="center demo-tab demo-timeline-chart-container" id="timelineTab" style="display: none">
<canvas id="timeline-chart"></canvas>
</div>
<div class="center demo-tab" id="qualityLevelControlTab" style="display: none;">
<div class="center demo-tab" id="qualityLevelControlTab" style="display: none">
<center>
<table>
<tr>
<td>
<p>Currently played level:</p>
</td>
<td>
<div id="currentLevelControl" style="display: inline;"></div>
</td>
</tr>
<tr>
<td>
<p>Next level loaded:</p>
</td>
<td>
<div id="nextLevelControl" style="display: inline;"></div>
</td>
</tr>
<tr>
<td>
<p>Currently loaded level:</p>
</td>
<td>
<div id="loadLevelControl" style="display: inline;"></div>
</td>
</tr>
<tr>
<td>
<p>Cap-limit level (maximum):</p>
</td>
<td>
<div id="levelCappingControl" style="display: inline;"></div>
</td>
</tr>
</table>
<table>
<tr>
<td>
<p>Currently played level:</p>
</td>
<td>
<div id="currentLevelControl" style="display: inline"></div>
</td>
</tr>
<tr>
<td>
<p>Next level loaded:</p>
</td>
<td>
<div id="nextLevelControl" style="display: inline"></div>
</td>
</tr>
<tr>
<td>
<p>Currently loaded level:</p>
</td>
<td>
<div id="loadLevelControl" style="display: inline"></div>
</td>
</tr>
<tr>
<td>
<p>Cap-limit level (maximum):</p>
</td>
<td>
<div id="levelCappingControl" style="display: inline"></div>
</td>
</tr>
</table>
</center>
</div>
<div class="center demo-tab" id="audioTrackControlTab" style="display: none;">
<div class="center demo-tab" id="audioTrackControlTab" style="display: none">
<table>
<tr>
<td>Current audio-track:</td>
<td><div id="audioTrackControl" style="display: inline;"></div></td>
<td><div id="audioTrackControl" style="display: inline"></div></td>
</tr>
<tr>
<td>Language / Name:</td>
<td><div id="audioTrackLabel" style="display: inline;">None selected</div></td>
<td><div id="audioTrackLabel" style="display: inline">None selected</div></td>
</tr>
</table>
</div>
<div class="center demo-tab" id="metricsDisplayTab" style="display: none;">
<br>
<div class="center demo-tab" id="metricsDisplayTab" style="display: none">
<br />
<div id="metricsButton">
<button type="button" class="btn btn-xs btn-info" onclick="$('#metricsButtonWindow').toggle();$('#metricsButtonFixed').toggle();windowSliding=!windowSliding; refreshCanvas()">toggle sliding/fixed window</button><br>
<button
type="button"
class="btn btn-xs btn-info"
onclick="$('#metricsButtonWindow').toggle();$('#metricsButtonFixed').toggle();windowSliding=!windowSliding; refreshCanvas()"
>
toggle sliding/fixed window</button
><br />
<div id="metricsButtonWindow">
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeSetSliding(0)">window ALL</button>
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeSetSliding(2000)">2s</button>
@@ -260,42 +268,48 @@
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeSetSliding(20000)">20s</button>
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeSetSliding(30000)">30s</button>
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeSetSliding(60000)">60s</button>
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeSetSliding(120000)">120s</button><br>
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeSetSliding(120000)">120s</button><br />
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeZoomIn()">Window Zoom In</button>
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeZoomOut()">Window Zoom Out</button><br>
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeSlideLeft()"> <<< Window Slide </button>
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeSlideRight()">Window Slide >>> </button><br>
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeZoomOut()">Window Zoom Out</button><br />
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeSlideLeft()"><<< Window Slide</button>
<button type="button" class="btn btn-xs btn-info" onclick="timeRangeSlideRight()">Window Slide >>></button><br />
</div>
<div id="metricsButtonFixed">
<button type="button" class="btn btn-xs btn-info" onclick="windowStart=$('#windowStart').val()">fixed window start(ms)</button>
<input type="text" id='windowStart' defaultValue="0" size="8" onkeydown="if(window.event.keyCode=='13'){windowStart=$('#windowStart').val();}">
<input type="text" id="windowStart" defaultValue="0" size="8" onkeydown="if(window.event.keyCode=='13'){windowStart=$('#windowStart').val();}" />
<button type="button" class="btn btn-xs btn-info" onclick="windowEnd=$('#windowEnd').val()">fixed window end(ms)</button>
<input type="text" id='windowEnd' defaultValue="10000" size="8" onkeydown="if(window.event.keyCode=='13'){windowEnd=$('#windowEnd').val();}"><br>
<input type="text" id="windowEnd" defaultValue="10000" size="8" onkeydown="if(window.event.keyCode=='13'){windowEnd=$('#windowEnd').val();}" /><br />
</div>
<button type="button" class="btn btn-xs btn-success" onclick="goToMetrics()" style="font-size:18px">metrics link</button>
<button type="button" class="btn btn-xs btn-success" onclick="goToMetricsPermaLink()" style="font-size:18px">metrics permalink</button>
<button type="button" class="btn btn-xs btn-success" onclick="copyMetricsToClipBoard()" style="font-size:18px">copy metrics to clipboard</button>
<canvas id="bufferTimerange_c" width="640" height="100" style="border:1px solid #000000" onmousedown="timeRangeCanvasonMouseDown(event)" onmousemove="timeRangeCanvasonMouseMove(event)" onmouseup="timeRangeCanvasonMouseUp(event)" onmouseout="timeRangeCanvasonMouseOut(event);"></canvas>
<canvas id="bitrateTimerange_c" width="640" height="100" style="border:1px solid #000000;"></canvas>
<canvas id="bufferWindow_c" width="640" height="100" style="border:1px solid #000000" onmousemove="windowCanvasonMouseMove(event);"></canvas>
<canvas id="videoEvent_c" width="640" height="15" style="border:1px solid #000000;"></canvas>
<canvas id="loadEvent_c" width="640" height="15" style="border:1px solid #000000;"></canvas><br>
<button type="button" class="btn btn-xs btn-success" onclick="goToMetrics()" style="font-size: 18px">metrics link</button>
<button type="button" class="btn btn-xs btn-success" onclick="goToMetricsPermaLink()" style="font-size: 18px">metrics permalink</button>
<button type="button" class="btn btn-xs btn-success" onclick="copyMetricsToClipBoard()" style="font-size: 18px">copy metrics to clipboard</button>
<canvas
id="bufferTimerange_c"
width="640"
height="100"
style="border: 1px solid #000000"
onmousedown="timeRangeCanvasonMouseDown(event)"
onmousemove="timeRangeCanvasonMouseMove(event)"
onmouseup="timeRangeCanvasonMouseUp(event)"
onmouseout="timeRangeCanvasonMouseOut(event);"
></canvas>
<canvas id="bitrateTimerange_c" width="640" height="100" style="border: 1px solid #000000"></canvas>
<canvas id="bufferWindow_c" width="640" height="100" style="border: 1px solid #000000" onmousemove="windowCanvasonMouseMove(event);"></canvas>
<canvas id="videoEvent_c" width="640" height="15" style="border: 1px solid #000000"></canvas>
<canvas id="loadEvent_c" width="640" height="15" style="border: 1px solid #000000"></canvas><br />
</div>
</div>
<div class="center demo-tab" id="statsDisplayTab" style="display: none;">
<br>
<div class="center demo-tab" id="statsDisplayTab" style="display: none">
<br />
<label>Buffer state:</label>
<pre id="bufferedOut"></pre>
<label>General stats:</label>
<pre id='statisticsOut'></pre>
<pre id="statisticsOut"></pre>
</div>
</div>
<footer>
<br><br><br><br><br><br>
</footer>
<footer><br /><br /><br /><br /><br /><br /></footer>
<!-- Demo page required libs -->
<script src="canvas.js"></script>
+349 -289
View File
File diff suppressed because it is too large Load Diff
+62 -52
View File
@@ -2,10 +2,10 @@
<head>
<title>hls.js metrics page</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap-theme.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap-theme.min.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
</head>
<body>
<div class="header-container">
@@ -13,11 +13,11 @@
<h1 class="title">hls.js metrics page</h1>
</header>
</div>
<pre id='HlsDate'></pre>
<pre id='StreamPermalink'></pre>
<input id="metricsData" class="innerControls" type=text value=""/>
window size
<div id="metricsButton">
<pre id="HlsDate"></pre>
<pre id="StreamPermalink"></pre>
<input id="metricsData" class="innerControls" type="text" value="" />
window size
<div id="metricsButton">
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeSetSliding(0)">window ALL</button>
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeSetSliding(2000)">2s</button>
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeSetSliding(5000)">5s</button>
@@ -25,62 +25,72 @@
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeSetSliding(20000)">20s</button>
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeSetSliding(30000)">30s</button>
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeSetSliding(60000)">60s</button>
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeSetSliding(120000)">120s</button><br>
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeSetSliding(120000)">120s</button><br />
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeZoomIn()">Window Zoom In</button>
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeZoomOut()">Window Zoom Out</button><br>
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeSlideLeft()"> <<< Window Slide </button>
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeSlideRight()">Window Slide >>> </button><br>
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeZoomOut()">Window Zoom Out</button><br />
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeSlideLeft()"><<< Window Slide</button>
<button type="button" class="btn btn-xs btn-primary" onclick="timeRangeSlideRight()">Window Slide >>></button><br />
<button type="button" class="btn btn-xs btn-primary" onclick="windowStart=$('#windowStart').val()">fixed window start(ms)</button>
<input type="text" id='windowStart' defaultValue="0" size="8" onkeydown="if(window.event.keyCode=='13'){windowStart=$('#windowStart').val();}">
<input type="text" id="windowStart" defaultValue="0" size="8" onkeydown="if(window.event.keyCode=='13'){windowStart=$('#windowStart').val();}" />
<button type="button" class="btn btn-xs btn-primary" onclick="windowEnd=$('#windowEnd').val()">fixed window end(ms)</button>
<input type="text" id='windowEnd' defaultValue="10000" size="8" onkeydown="if(window.event.keyCode=='13'){windowEnd=$('#windowEnd').val();}"><br>
<canvas id="bufferTimerange_c" width="640" height="100" style="border:1px solid #000000" onmousedown="timeRangeCanvasonMouseDown(event)" onmousemove="timeRangeCanvasonMouseMove(event)" onmouseup="timeRangeCanvasonMouseUp(event)" onmouseout="timeRangeCanvasonMouseOut(event)";></canvas>
<canvas id="bitrateTimerange_c" width="640" height="100" style="border:1px solid #000000";></canvas>
<canvas id="bufferWindow_c" width="640" height="100" style="border:1px solid #000000" onmousemove="windowCanvasonMouseMove(event)";></canvas>
<canvas id="videoEvent_c" width="640" height="15" style="border:1px solid #000000";></canvas>
<canvas id="loadEvent_c" width="640" height="15" style="border:1px solid #000000";></canvas><br>
<input type="text" id="windowEnd" defaultValue="10000" size="8" onkeydown="if(window.event.keyCode=='13'){windowEnd=$('#windowEnd').val();}" /><br />
<canvas
id="bufferTimerange_c"
width="640"
height="100"
style="border: 1px solid #000000"
onmousedown="timeRangeCanvasonMouseDown(event)"
onmousemove="timeRangeCanvasonMouseMove(event)"
onmouseup="timeRangeCanvasonMouseUp(event)"
onmouseout="timeRangeCanvasonMouseOut(event)"
;
></canvas>
<canvas id="bitrateTimerange_c" width="640" height="100" style="border: 1px solid #000000" ;></canvas>
<canvas id="bufferWindow_c" width="640" height="100" style="border: 1px solid #000000" onmousemove="windowCanvasonMouseMove(event)" ;></canvas>
<canvas id="videoEvent_c" width="640" height="15" style="border: 1px solid #000000" ;></canvas>
<canvas id="loadEvent_c" width="640" height="15" style="border: 1px solid #000000" ;></canvas><br />
</div>
<script src="canvas.js"></script>
<script src="metrics.js"></script>
<script src="libs/jsonpack.js"></script>
<script>
$(document).ready(function () {
$('#metricsData').change(function () {
events = jsonpack.unpack(atob($('#metricsData').val()));
updateMetrics();
});
});
var data = location.search.split('data=')[1] || location.hash.split('data=')[1],
events;
if (data) {
events = jsonpack.unpack(atob(decodeURIComponent(data)));
updateMetrics();
}
$(document).ready(function() {
$('#metricsData').change(function() { events = jsonpack.unpack(atob($('#metricsData').val())); updateMetrics(); });
});
function updateMetrics() {
var hlsLink = new URL('index.html?src=' + encodeURIComponent(events.url), window.location.href).href;
var playlistLink = document.createElement('a');
playlistLink.setAttribute('href', events.url);
playlistLink.textContent = events.url;
var data = location.search.split('data=')[1] || location.hash.split('data=')[1],events;
if (data) {
events = jsonpack.unpack(atob(decodeURIComponent(data)));
updateMetrics();
}
var replayLink = document.createElement('a');
replayLink.setAttribute('href', hlsLink);
replayLink.textContent = hlsLink;
function updateMetrics() {
var hlsLink = new URL('index.html?src=' + encodeURIComponent(events.url), window.location.href).href;
var playlistLink = document.createElement("a");
playlistLink.setAttribute("href", events.url);
playlistLink.textContent = events.url;
var replayLink = document.createElement("a");
replayLink.setAttribute("href", hlsLink);
replayLink.textContent = hlsLink;
var fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode('playlist: '));
fragment.appendChild(playlistLink);
fragment.appendChild(document.createElement("br"));
fragment.appendChild(document.createTextNode('replay: '));
fragment.appendChild(replayLink);
$("#StreamPermalink").html(fragment);
$("#HlsDate").text("session Start Date:" + new Date(events.t0));
metricsDisplayed=true;
showMetrics();
refreshCanvas();
}
var fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode('playlist: '));
fragment.appendChild(playlistLink);
fragment.appendChild(document.createElement('br'));
fragment.appendChild(document.createTextNode('replay: '));
fragment.appendChild(replayLink);
$('#StreamPermalink').html(fragment);
$('#HlsDate').text('session Start Date:' + new Date(events.t0));
metricsDisplayed = true;
showMetrics();
refreshCanvas();
}
</script>
</body>
+20 -23
View File
@@ -12,15 +12,12 @@ var timeRangeMouseDown = false;
$('#windowStart').val(windowStart);
$('#windowEnd').val(windowEnd);
function showMetrics () {
function showMetrics() {
metricsDisplayed = true;
var width = window.innerWidth - 30;
$('#bufferWindow_c')[0].width =
$('#bitrateTimerange_c')[0].width =
$('#bufferTimerange_c')[0].width =
$('#videoEvent_c')[0].width =
$('#metricsButton')[0].width =
$('#loadEvent_c')[0].width = width;
$('#bufferWindow_c')[0].width = $('#bitrateTimerange_c')[0].width = $('#bufferTimerange_c')[0].width = $('#videoEvent_c')[0].width = $('#metricsButton')[0].width = $(
'#loadEvent_c'
)[0].width = width;
$('#bufferWindow_c').show();
$('#bitrateTimerange_c').show();
$('#bufferTimerange_c').show();
@@ -29,7 +26,7 @@ function showMetrics () {
$('#loadEvent_c').show();
}
function hideMetrics () {
function hideMetrics() {
metricsDisplayed = false;
$('#bufferWindow_c').hide();
$('#bitrateTimerange_c').hide();
@@ -39,17 +36,17 @@ function hideMetrics () {
$('#loadEvent_c').hide();
}
function timeRangeSetSliding (duration) {
function timeRangeSetSliding(duration) {
windowDuration = duration;
windowSliding = true;
refreshCanvas();
}
function timeRangeCanvasonMouseDown (evt) {
function timeRangeCanvasonMouseDown(evt) {
var canvas = evt.currentTarget;
var bRect = canvas.getBoundingClientRect();
var mouseX = Math.round((evt.clientX - bRect.left) * (canvas.width / bRect.width));
windowStart = Math.max(0, Math.round((mouseX - eventLeftMargin) * getWindowTimeRange().now / (canvas.width - eventLeftMargin)));
windowStart = Math.max(0, Math.round(((mouseX - eventLeftMargin) * getWindowTimeRange().now) / (canvas.width - eventLeftMargin)));
windowEnd = windowStart + 500;
timeRangeMouseDown = true;
windowSliding = false;
@@ -59,12 +56,12 @@ function timeRangeCanvasonMouseDown (evt) {
refreshCanvas();
}
function timeRangeCanvasonMouseMove (evt) {
function timeRangeCanvasonMouseMove(evt) {
if (timeRangeMouseDown) {
var canvas = evt.currentTarget;
var bRect = canvas.getBoundingClientRect();
var mouseX = Math.round((evt.clientX - bRect.left) * (canvas.width / bRect.width));
var pos = Math.max(0, Math.round((mouseX - eventLeftMargin) * getWindowTimeRange().now / (canvas.width - eventLeftMargin)));
var pos = Math.max(0, Math.round(((mouseX - eventLeftMargin) * getWindowTimeRange().now) / (canvas.width - eventLeftMargin)));
if (pos < windowStart) {
windowStart = pos;
} else {
@@ -81,25 +78,25 @@ function timeRangeCanvasonMouseMove (evt) {
}
}
function timeRangeCanvasonMouseUp (evt) {
function timeRangeCanvasonMouseUp(evt) {
timeRangeMouseDown = false;
}
function timeRangeCanvasonMouseOut (evt) {
function timeRangeCanvasonMouseOut(evt) {
timeRangeMouseDown = false;
}
function windowCanvasonMouseMove (evt) {
function windowCanvasonMouseMove(evt) {
var canvas = evt.currentTarget;
var bRect = canvas.getBoundingClientRect();
var mouseX = Math.round((evt.clientX - bRect.left) * (canvas.width / bRect.width));
var timeRange = getWindowTimeRange();
windowFocus = timeRange.min + Math.max(0, Math.round((mouseX - eventLeftMargin) * (timeRange.max - timeRange.min) / (canvas.width - eventLeftMargin)));
windowFocus = timeRange.min + Math.max(0, Math.round(((mouseX - eventLeftMargin) * (timeRange.max - timeRange.min)) / (canvas.width - eventLeftMargin)));
// console.log(windowFocus);
refreshCanvas();
}
function refreshCanvas () {
function refreshCanvas() {
if (metricsDisplayed) {
try {
var windowTime = getWindowTimeRange();
@@ -119,7 +116,7 @@ function refreshCanvas () {
}
}
function getWindowTimeRange () {
function getWindowTimeRange() {
var tnow;
var minTime;
var maxTime;
@@ -147,7 +144,7 @@ function getWindowTimeRange () {
return { min: minTime, max: maxTime, now: tnow, focus: windowFocus };
}
function timeRangeZoomIn () {
function timeRangeZoomIn() {
if (windowSliding) {
windowDuration /= 2;
} else {
@@ -163,7 +160,7 @@ function timeRangeZoomIn () {
refreshCanvas();
}
function timeRangeZoomOut () {
function timeRangeZoomOut() {
if (windowSliding) {
windowDuration *= 2;
} else {
@@ -178,7 +175,7 @@ function timeRangeZoomOut () {
refreshCanvas();
}
function timeRangeSlideLeft () {
function timeRangeSlideLeft() {
var duration = windowEnd - windowStart;
windowStart -= duration / 4;
windowEnd -= duration / 4;
@@ -189,7 +186,7 @@ function timeRangeSlideLeft () {
refreshCanvas();
}
function timeRangeSlideRight () {
function timeRangeSlideRight() {
var duration = windowEnd - windowStart;
windowStart += duration / 4;
windowEnd += duration / 4;
+11 -9
View File
@@ -2,7 +2,8 @@ header {
text-align: center;
}
th, td {
th,
td {
padding: 15px;
}
@@ -28,11 +29,14 @@ select option {
background-color: #111;
}
button, optgroup, select {
button,
optgroup,
select {
background-color: rgb(0, 40, 70);
}
input, textarea {
input,
textarea {
background-color: #222;
}
@@ -153,7 +157,7 @@ select option {
width: 720px;
margin-left: auto;
margin-right: auto;
display: block
display: block;
}
.center {
@@ -162,12 +166,12 @@ select option {
overflow: hidden;
margin-left: auto;
margin-right: auto;
display: block
display: block;
}
#toggleButtons button {
width: 16%;
display : inline-block;
display: inline-block;
text-align: center;
font-size: 10pt;
font-weight: bolder;
@@ -189,7 +193,6 @@ select option {
overflow: auto;
}
#streamURL,
#streamSelect {
width: calc(100% - 4px);
@@ -209,7 +212,7 @@ select option {
flex: 1 1 auto;
min-width: 0;
overflow-wrap: break-word;
overflow: hidden; /* for IE11 */
overflow: hidden; /* for IE11 */
padding: 10px 0 0 10px;
}
@@ -233,5 +236,4 @@ select option {
border-top: 0;
border-left: solid 1px #ccc;
}
}
+410 -417
View File
File diff suppressed because it is too large Load Diff
+238 -239
View File
@@ -2,218 +2,218 @@
design idea is pretty simple :
- main functionalities are split into several subsystems
- all subsystems are instantiated by the Hls instance.
- each subsystem heavily relies on events for internal/external communications.
- Events are handled using [EventEmitter3](https://github.com/primus/eventemitter3)
- bundled for the browser by [webpack](https://webpack.js.org/)
- main functionalities are split into several subsystems
- all subsystems are instantiated by the Hls instance.
- each subsystem heavily relies on events for internal/external communications.
- Events are handled using [EventEmitter3](https://github.com/primus/eventemitter3)
- bundled for the browser by [webpack](https://webpack.js.org/)
## Code structure
- [src/config.js][]
- definition of default Hls Config. entry point for conditional compilation (altaudio/subtitle)
- [src/errors.js][]
- definition of Hls.ErrorTypes and Hls.ErrorDetails
- [src/event-handler.ts][]
- helper class simplifying Hls event handling, event error catching
- [src/events.js][]
- definition of Hls.Events
- [src/hls.js][]
- definition of Hls Class. instantiate all subcomponents. conditionally instantiate optional subcomponents.
- [src/index.js][]
- needed for ES6 export
- [src/controller/abr-controller.js][]
- in charge of determining auto quality level.
- auto quality switch algorithm is bitrate based : fragment loading bitrate is monitored and smoothed using 2 exponential weighted moving average (a fast one, to adapt quickly on bandwidth drop and a slow one, to avoid ramping up too quickly on bandwidth increase)
- in charge of **monitoring fragment loading speed** (by monitoring the amount of data received from fragment loader `stats.loaded` counter)
- "expected time of fragment load completion" is computed using "fragment loading instant bandwidth".
- this time is compared to the "expected time of buffer starvation".
- if we have less than 2 fragments buffered and if "expected time of fragment load completion" is bigger than "expected time of buffer starvation" and also bigger than duration needed to load fragment at next quality level (determined by auto quality switch algorithm), current fragment loading is aborted, and a FRAG_LOAD_EMERGENCY_ABORTED event is triggered. this event will be handled by stream-controller.
- [src/controller/audio-stream-controller.js][]
- audio stream controller is in charge of filling audio buffer in case alternate audio tracks are used
- if buffer is not filled up appropriately (i.e. as per defined maximum buffer size, it will trigger the following actions:
- retrieve "not buffered" media position greater then current playback position. this is performed by comparing audio sourcebuffer.buffered and media.currentTime.
- retrieve URL of fragment matching with this media position, and appropriate audio track
- trigger KEY_LOADING event (only if fragment is encrypted)
- trigger FRAG_LOADING event
- **trigger fragment demuxing** on FRAG_LOADED
- trigger BUFFER_CODECS on FRAG_PARSING_INIT_SEGMENT
- trigger BUFFER_APPENDING on FRAG_PARSING_DATA
- once FRAG_PARSED is received an all segments have been appended (BUFFER_APPENDED) then audio stream controller will recheck whether it needs to buffer more data.
- [src/controller/audio-track-controller.js][]
- audio track controller is handling alternate audio track set/get ((re)loading tracks/switching)
- [src/controller/buffer-controller.js][]
- in charge of:
- resetting media buffer upon BUFFER_RESET event reception
- initializing [SourceBuffer](http://www.w3.org/TR/media-source/#sourcebuffer) with appropriate codecs info upon BUFFER_CODECS event reception
- appending MP4 boxes in [SourceBuffer](http://www.w3.org/TR/media-source/#sourcebuffer) upon BUFFER_APPENDING
- trigger BUFFER_APPENDED event upon successful buffer appending
- flushing specified buffer range upon reception of BUFFER_FLUSHING event
- trigger BUFFER_FLUSHED event upon successful buffer flushing
- [src/controller/cap-level-controller.js][]
- in charge of determining best quality level to actual size (dimensions: width and height) of the player
- [src/controller/fps-controller.js][]
- in charge of monitoring frame rate, and fire FPS_DROP event in case FPS drop exceeds configured threshold. disabled for now.
- [src/controller/id3-track-controller.js](../src/controller/id3-track-controller.js)
- in charge of creating the id3 metadata text track and adding cues to that track in response to the FRAG_PARSING_METADATA event. the raw id3 data is base64 encoded and stored in the cue's text property.
- [src/controller/level-controller.js][]
- handling quality level set/get ((re)loading stream manifest/switching levels)
- in charge of scheduling playlist (re)loading
- monitors fragment and key loading errors. Performs fragment hunt by switching between primary and backup streams and down-shifting a level till `fragLoadingMaxRetry` limit is reached.
- monitors level loading errors. Performs level hunt by switching between primary and backup streams and down-shifting a level till `levelLoadingMaxRetry` limit is reached.
- periodically refresh active live playlist
**Feature: Media Zigzagging**
If there is a backup stream, Media Zigzagging will go through all available levels in `primary` and `backup` streams. Behavior has a dual constraint, where fragment retry limits and level limits are accounted in the same time.
When the lowest level has been reached, zigzagging will be adjusted to start from the highest level until retry limits are not reached.
![Media Zigzagging Explanation](./media-zigzagging.png)
Where: F - Bad Fragment, L - Bad Level
**Retry Recommendations**
By not having multiple renditions, recovery logic will not be able to add extra value to your platform. In order to have good results for dual constraint media hunt, specify big enough limits for fragments and levels retries.
- Level: don't use total retry less than `3 - 4`
- Fragment: don't use total retry less than `4 - 6`
- Implement short burst retries (i.e. small retry delay `0.5 - 4` seconds), and when library returns fatal error switch to a different CDN
- [src/config.js][]
- definition of default Hls Config. entry point for conditional compilation (altaudio/subtitle)
- [src/errors.js][]
- definition of Hls.ErrorTypes and Hls.ErrorDetails
- [src/event-handler.ts][]
- helper class simplifying Hls event handling, event error catching
- [src/events.js][]
- definition of Hls.Events
- [src/hls.js][]
- definition of Hls Class. instantiate all subcomponents. conditionally instantiate optional subcomponents.
- [src/index.js][]
- needed for ES6 export
- [src/controller/abr-controller.js][]
- in charge of determining auto quality level.
- auto quality switch algorithm is bitrate based : fragment loading bitrate is monitored and smoothed using 2 exponential weighted moving average (a fast one, to adapt quickly on bandwidth drop and a slow one, to avoid ramping up too quickly on bandwidth increase)
- in charge of **monitoring fragment loading speed** (by monitoring the amount of data received from fragment loader `stats.loaded` counter)
- "expected time of fragment load completion" is computed using "fragment loading instant bandwidth".
- this time is compared to the "expected time of buffer starvation".
- if we have less than 2 fragments buffered and if "expected time of fragment load completion" is bigger than "expected time of buffer starvation" and also bigger than duration needed to load fragment at next quality level (determined by auto quality switch algorithm), current fragment loading is aborted, and a FRAG_LOAD_EMERGENCY_ABORTED event is triggered. this event will be handled by stream-controller.
- [src/controller/audio-stream-controller.js][]
- audio stream controller is in charge of filling audio buffer in case alternate audio tracks are used
- if buffer is not filled up appropriately (i.e. as per defined maximum buffer size, it will trigger the following actions:
- retrieve "not buffered" media position greater then current playback position. this is performed by comparing audio sourcebuffer.buffered and media.currentTime.
- retrieve URL of fragment matching with this media position, and appropriate audio track
- trigger KEY_LOADING event (only if fragment is encrypted)
- trigger FRAG_LOADING event
- **trigger fragment demuxing** on FRAG_LOADED
- trigger BUFFER_CODECS on FRAG_PARSING_INIT_SEGMENT
- trigger BUFFER_APPENDING on FRAG_PARSING_DATA
- once FRAG_PARSED is received an all segments have been appended (BUFFER_APPENDED) then audio stream controller will recheck whether it needs to buffer more data.
- [src/controller/audio-track-controller.js][]
- audio track controller is handling alternate audio track set/get ((re)loading tracks/switching)
- [src/controller/buffer-controller.js][]
- in charge of:
- resetting media buffer upon BUFFER_RESET event reception
- initializing [SourceBuffer](http://www.w3.org/TR/media-source/#sourcebuffer) with appropriate codecs info upon BUFFER_CODECS event reception
- appending MP4 boxes in [SourceBuffer](http://www.w3.org/TR/media-source/#sourcebuffer) upon BUFFER_APPENDING
- trigger BUFFER_APPENDED event upon successful buffer appending
- flushing specified buffer range upon reception of BUFFER_FLUSHING event
- trigger BUFFER_FLUSHED event upon successful buffer flushing
- [src/controller/cap-level-controller.js][]
- in charge of determining best quality level to actual size (dimensions: width and height) of the player
- [src/controller/fps-controller.js][]
- in charge of monitoring frame rate, and fire FPS_DROP event in case FPS drop exceeds configured threshold. disabled for now.
- [src/controller/id3-track-controller.js](../src/controller/id3-track-controller.js)
- in charge of creating the id3 metadata text track and adding cues to that track in response to the FRAG_PARSING_METADATA event. the raw id3 data is base64 encoded and stored in the cue's text property.
- [src/controller/level-controller.js][]
- [src/controller/stream-controller.js][]
- stream controller is in charge of:
- triggering BUFFER_RESET on MANIFEST_PARSED or startLoad()
- **ensuring that media buffer is filled as per defined quality selection logic**.
- if buffer is not filled up appropriately (i.e. as per defined maximum buffer size, or as per defined quality level), stream controller will trigger the following actions:
- retrieve "not buffered" media position greater then current playback position. this is performed by comparing video.buffered and video.currentTime.
- if there are holes in video.buffered, smaller than config.maxBufferHole, they will be ignored.
- retrieve appropriate quality level through hls.nextLoadLevel getter
- then setting hls.nextLoadLevel (this will force level-controller to retrieve level details if not available yet)
- retrieve fragment (and its URL) matching with this media position, using binary search on level details
- trigger KEY_LOADING event (only if fragment is encrypted)
- trigger FRAG_LOADING event
- **trigger fragment demuxing** on FRAG_LOADED
- trigger BUFFER_CODECS on FRAG_PARSING_INIT_SEGMENT
- trigger BUFFER_APPENDING on FRAG_PARSING_DATA
- once FRAG_PARSED is received an all segments have been appended (BUFFER_APPENDED) then buffer controller will recheck whether it needs to buffer more data.
- **monitor current playback quality level** (buffer controller maintains a map between media position and quality level)
- **monitor playback progress** : if playhead is not moving for more than `config.highBufferWatchdogPeriod` although it should (video metadata is known and video is not ended, nor paused, nor in seeking state) and if we have less than 500ms buffered upfront, one of two things will happen.
- if there is a known malformed fragment then hls.js will **jump over the buffer hole** and seek to the beginning the next playable buffered range.
- hls.js will nudge currentTime until playback recovers (it will retry every seconds, and report a fatal error after config.maxNudgeRetry retries)
500 ms is a "magic number" that has been set to overcome browsers not always stopping playback at the exact end of a buffered range.
these holes in media buffered are often encountered on stream discontinuity or on quality level switch. holes could be "large" especially if fragments are not starting with a keyframe.
- convert non-fatal `FRAG_LOAD_ERROR`/`FRAG_LOAD_TIMEOUT`/`KEY_LOAD_ERROR`/`KEY_LOAD_TIMEOUT` error into fatal error when media position is not buffered and max load retry has been reached
- stream controller actions are scheduled by a tick timer (invoked every 100ms) and actions are controlled by a state machine.
- [src/controller/subtitle-stream-controller.js][]
- subtitle stream controller is in charge of processing subtitle track fragments
- subtitle stream controller takes the following actions:
- once a SUBTITLE_TRACK_LOADED is received, the controller will begin processing the subtitle fragments
- trigger KEY_LOADING event if fragment is encrypted
- trigger FRAG_LOADING event
- invoke decrypter.decrypt method on FRAG_LOADED if frag is encrypted
- trigger FRAG_DECRYPTED event once encrypted fragment is decrypted
- [src/controller/subtitle-track-controller.js][]
- subtitle track controller handles subtitle track loading and switching
- [src/controller/timeline-controller.js][]
- Manages pulling CEA-708 caption data from the fragments, running them through the cea-608-parser, and handing them off to a display class, which defaults to src/utils/cues.js
- [src/crypt/aes-crypto.js][]
- AES 128 software decryption routine, low level class handling decryption of 128 bit of data.
- [src/crypt/aes-decrypter.js][]
- AES 128-CBC software decryption routine, high-level class handling cipher-block chaining (CBC), handles PKCS7 padding when the option is enabled.
- [src/crypt/decrypter.js][]
- decrypter interface, use either WebCrypto API if available and enabled, or fallback on AES 128 software decryption routine.
- [src/demux/aacdemuxer.js][]
- AAC ES demuxer
- extract ADTS samples from AAC ES
- [src/demux/adts.js][]
- ADTS header parser helper, extract audio config from ADTS header. used by AAC ES and TS demuxer.
- [src/demux/demuxer.js][]
- demuxer abstraction interface, that will either use a [Worker](https://en.wikipedia.org/wiki/Web_worker) to demux or demux inline depending on config/browser capabilities.
- also handle fragment decryption using WebCrypto API (fragment decryption is performed in main thread)
- if Worker are disabled. demuxing will be performed in the main thread.
- if Worker are available/enabled,
- demuxer will instantiate a Worker
- post/listen to Worker message,
- and redispatch events as expected by hls.js.
- Fragments are sent as [transferable objects](https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast) in order to minimize message passing overhead.
- [src/demux/demuxer-inline.js][]
- inline demuxer.
- probe fragments and instantiate appropriate demuxer depending on content type (TSDemuxer, AACDemuxer, ...)
- [src/demux/demuxer-worker.js][]
- demuxer web worker.
- listen to worker message, and trigger DemuxerInline upon reception of Fragments.
- provides MP4 Boxes back to main thread using [transferable objects](https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast) in order to minimize message passing overhead.
- [src/demux/exp-golomb.js][]
- utility class to extract Exponential-Golomb coded data. needed by TS demuxer for SPS parsing.
- [src/demux/id3.ts][]
- utility class that detect and parse ID3 tags, used by AAC demuxer
- [src/demux/tsdemuxer.js][]
- highly optimized TS demuxer:
- parse PAT, PMT
- extract PES packet from audio and video PIDs
- extract AVC/H264 NAL units,AAC/ADTS samples, MP3/MPEG audio samples from PES packet
- trigger the remuxer upon parsing completion
- it also tries to workaround as best as it can audio codec switch (HE-AAC to AAC and vice versa), without having to restart the MediaSource.
- it also controls the remuxing process :
- upon discontinuity or level switch detection, it will also notifies the remuxer so that it can reset its state.
- [src/demux/sample-aes.js][]
- sample aes decrypter
- [src/helper/aac.js][]
- helper class to create silent AAC frames (useful to handle streams with audio holes)
- [src/helper/buffer-helper.js][]
- helper class, providing methods dealing buffer length retrieval (given a media position, it will return the upfront buffer length, next buffer position ...)
- [src/helper/level-helper.js][]
- helper class providing methods dealing with playlist sliding and fragment duration drift computation : after fragment parsing, start/end fragment timestamp will be used to adjust potential playlist drifts and live playlist sliding.
- [src/helper/fragment-tracker.js][]
- in charge of checking if a fragment was successfully loaded into the buffer
- tracks which parts of the buffer is not loaded correctly
- tracks which parts of the buffer was unloaded by the coded frame eviction algorithm
- [src/loader/fragment-loader.js][]
- in charge of loading fragments, use xhr-loader if not overrided by user config
- [src/loader/key-loader.js][]
- in charge of loading decryption key
- [src/loader/playlist-loader.js][]
- in charge of loading manifest, and level playlists, use xhr-loader if not overrided by user config.
- [src/remux/dummy-remuxer.js][]
- example dummy remuxer
- [src/remux/mp4-generator.js][]
- in charge of generating MP4 boxes
- generate Init Segment (moov)
- generate samples Box (moof and mdat)
- [src/remux/mp4-remuxer.js][]
- in charge of converting AVC/AAC/MP3 samples provided by demuxer into fragmented ISO BMFF boxes, compatible with MediaSource
- this remuxer is able to deal with small gaps between fragments and ensure timestamp continuity. it is also able to create audio padding (silent AAC audio frames) in case there is a significant audio 'hole' in the stream.
- it notifies remuxing completion using events (```FRAG_PARSING_INIT_SEGMENT```, ```FRAG_PARSING_DATA``` and ```FRAG_PARSED```)
- [src/utils/attr-list.js][]
- Attribute List parsing helper class, used by playlist-loader
- [src/utils/binary-search.js][]
- binary search helper class
- [src/utils/cea-608-parser.js][]
- Port of dash.js class of the same name to ECMAScript. This class outputs "Screen" objects which contain rows of characters that can be rendered by a separate class.
- [src/utils/cues.js][]
- Default CC renderer. Translates Screen objects from cea-608-parser into HTML5 VTTCue objects, rendered by the video tag
- [src/utils/ewma.js][]
- compute [exponential weighted moving average](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average)
- [src/utils/ewma-bandwidth-estimator.js][]
- Exponential Weighted Moving Average bandwidth estimator, heavily inspired from shaka-player
- Tracks bandwidth samples and estimates available bandwidth, based on the minimum of two exponentially-weighted moving averages with different half-lives.
- one fast average with a short half-life: useful to quickly react to bandwidth drop and switch rendition down quickly
- one slow average with a long half-life: useful to slowly react to bandwidth increase and avoid switching up rendition to quickly
- bandwidth estimate is Math.min(fast average,slow average)
- average half-life are configurable , refer to abrEwma* config params
- [src/utils/hex.js][]
- Hex dump utils, useful for debug
- [src/utils/logger.js][]
- logging utils, useful for debug
- [src/utils/polyfill.js][]
- ArrayBuffer.slice polyfill
- [src/utils/url.js][]
- convert base+relative URL into absolute URL
- [src/utils/xhr-loader.js][]
- XmlHttpRequest wrapper. it handles standard HTTP GET but also retries and timeout.
- retries : if xhr fails, HTTP GET will be retried after a predetermined delay. this delay is increasing following an exponential backoff. after a predetemined max number of retries, an error callback will be triggered.
- timeout: if load exceeds max allowed duration, a timeout callback will be triggered. it is up to the callback to decides whether the connection should be cancelled or not.
- handling quality level set/get ((re)loading stream manifest/switching levels)
- in charge of scheduling playlist (re)loading
- monitors fragment and key loading errors. Performs fragment hunt by switching between primary and backup streams and down-shifting a level till `fragLoadingMaxRetry` limit is reached.
- monitors level loading errors. Performs level hunt by switching between primary and backup streams and down-shifting a level till `levelLoadingMaxRetry` limit is reached.
- periodically refresh active live playlist
**Feature: Media Zigzagging**
If there is a backup stream, Media Zigzagging will go through all available levels in `primary` and `backup` streams. Behavior has a dual constraint, where fragment retry limits and level limits are accounted in the same time.
When the lowest level has been reached, zigzagging will be adjusted to start from the highest level until retry limits are not reached.
![Media Zigzagging Explanation](./media-zigzagging.png)
Where: F - Bad Fragment, L - Bad Level
**Retry Recommendations**
By not having multiple renditions, recovery logic will not be able to add extra value to your platform. In order to have good results for dual constraint media hunt, specify big enough limits for fragments and levels retries.
- Level: don't use total retry less than `3 - 4`
- Fragment: don't use total retry less than `4 - 6`
- Implement short burst retries (i.e. small retry delay `0.5 - 4` seconds), and when library returns fatal error switch to a different CDN
- [src/controller/stream-controller.js][]
- stream controller is in charge of:
- triggering BUFFER_RESET on MANIFEST_PARSED or startLoad()
- **ensuring that media buffer is filled as per defined quality selection logic**.
- if buffer is not filled up appropriately (i.e. as per defined maximum buffer size, or as per defined quality level), stream controller will trigger the following actions:
- retrieve "not buffered" media position greater then current playback position. this is performed by comparing video.buffered and video.currentTime.
- if there are holes in video.buffered, smaller than config.maxBufferHole, they will be ignored.
- retrieve appropriate quality level through hls.nextLoadLevel getter
- then setting hls.nextLoadLevel (this will force level-controller to retrieve level details if not available yet)
- retrieve fragment (and its URL) matching with this media position, using binary search on level details
- trigger KEY_LOADING event (only if fragment is encrypted)
- trigger FRAG_LOADING event
- **trigger fragment demuxing** on FRAG_LOADED
- trigger BUFFER_CODECS on FRAG_PARSING_INIT_SEGMENT
- trigger BUFFER_APPENDING on FRAG_PARSING_DATA
- once FRAG_PARSED is received an all segments have been appended (BUFFER_APPENDED) then buffer controller will recheck whether it needs to buffer more data.
- **monitor current playback quality level** (buffer controller maintains a map between media position and quality level)
- **monitor playback progress** : if playhead is not moving for more than `config.highBufferWatchdogPeriod` although it should (video metadata is known and video is not ended, nor paused, nor in seeking state) and if we have less than 500ms buffered upfront, one of two things will happen.
- if there is a known malformed fragment then hls.js will **jump over the buffer hole** and seek to the beginning the next playable buffered range.
- hls.js will nudge currentTime until playback recovers (it will retry every seconds, and report a fatal error after config.maxNudgeRetry retries)
500 ms is a "magic number" that has been set to overcome browsers not always stopping playback at the exact end of a buffered range.
these holes in media buffered are often encountered on stream discontinuity or on quality level switch. holes could be "large" especially if fragments are not starting with a keyframe.
- convert non-fatal `FRAG_LOAD_ERROR`/`FRAG_LOAD_TIMEOUT`/`KEY_LOAD_ERROR`/`KEY_LOAD_TIMEOUT` error into fatal error when media position is not buffered and max load retry has been reached
- stream controller actions are scheduled by a tick timer (invoked every 100ms) and actions are controlled by a state machine.
- [src/controller/subtitle-stream-controller.js][]
- subtitle stream controller is in charge of processing subtitle track fragments
- subtitle stream controller takes the following actions:
- once a SUBTITLE_TRACK_LOADED is received, the controller will begin processing the subtitle fragments
- trigger KEY_LOADING event if fragment is encrypted
- trigger FRAG_LOADING event
- invoke decrypter.decrypt method on FRAG_LOADED if frag is encrypted
- trigger FRAG_DECRYPTED event once encrypted fragment is decrypted
- [src/controller/subtitle-track-controller.js][]
- subtitle track controller handles subtitle track loading and switching
- [src/controller/timeline-controller.js][]
- Manages pulling CEA-708 caption data from the fragments, running them through the cea-608-parser, and handing them off to a display class, which defaults to src/utils/cues.js
- [src/crypt/aes-crypto.js][]
- AES 128 software decryption routine, low level class handling decryption of 128 bit of data.
- [src/crypt/aes-decrypter.js][]
- AES 128-CBC software decryption routine, high-level class handling cipher-block chaining (CBC), handles PKCS7 padding when the option is enabled.
- [src/crypt/decrypter.js][]
- decrypter interface, use either WebCrypto API if available and enabled, or fallback on AES 128 software decryption routine.
- [src/demux/aacdemuxer.js][]
- AAC ES demuxer
- extract ADTS samples from AAC ES
- [src/demux/adts.js][]
- ADTS header parser helper, extract audio config from ADTS header. used by AAC ES and TS demuxer.
- [src/demux/demuxer.js][]
- demuxer abstraction interface, that will either use a [Worker](https://en.wikipedia.org/wiki/Web_worker) to demux or demux inline depending on config/browser capabilities.
- also handle fragment decryption using WebCrypto API (fragment decryption is performed in main thread)
- if Worker are disabled. demuxing will be performed in the main thread.
- if Worker are available/enabled,
- demuxer will instantiate a Worker
- post/listen to Worker message,
- and redispatch events as expected by hls.js.
- Fragments are sent as [transferable objects](https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast) in order to minimize message passing overhead.
- [src/demux/demuxer-inline.js][]
- inline demuxer.
- probe fragments and instantiate appropriate demuxer depending on content type (TSDemuxer, AACDemuxer, ...)
- [src/demux/demuxer-worker.js][]
- demuxer web worker.
- listen to worker message, and trigger DemuxerInline upon reception of Fragments.
- provides MP4 Boxes back to main thread using [transferable objects](https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast) in order to minimize message passing overhead.
- [src/demux/exp-golomb.js][]
- utility class to extract Exponential-Golomb coded data. needed by TS demuxer for SPS parsing.
- [src/demux/id3.ts][]
- utility class that detect and parse ID3 tags, used by AAC demuxer
- [src/demux/tsdemuxer.js][]
- highly optimized TS demuxer:
- parse PAT, PMT
- extract PES packet from audio and video PIDs
- extract AVC/H264 NAL units,AAC/ADTS samples, MP3/MPEG audio samples from PES packet
- trigger the remuxer upon parsing completion
- it also tries to workaround as best as it can audio codec switch (HE-AAC to AAC and vice versa), without having to restart the MediaSource.
- it also controls the remuxing process :
- upon discontinuity or level switch detection, it will also notifies the remuxer so that it can reset its state.
- [src/demux/sample-aes.js][]
- sample aes decrypter
- [src/helper/aac.js][]
- helper class to create silent AAC frames (useful to handle streams with audio holes)
- [src/helper/buffer-helper.js][]
- helper class, providing methods dealing buffer length retrieval (given a media position, it will return the upfront buffer length, next buffer position ...)
- [src/helper/level-helper.js][]
- helper class providing methods dealing with playlist sliding and fragment duration drift computation : after fragment parsing, start/end fragment timestamp will be used to adjust potential playlist drifts and live playlist sliding.
- [src/helper/fragment-tracker.js][]
- in charge of checking if a fragment was successfully loaded into the buffer
- tracks which parts of the buffer is not loaded correctly
- tracks which parts of the buffer was unloaded by the coded frame eviction algorithm
- [src/loader/fragment-loader.js][]
- in charge of loading fragments, use xhr-loader if not overrided by user config
- [src/loader/key-loader.js][]
- in charge of loading decryption key
- [src/loader/playlist-loader.js][]
- in charge of loading manifest, and level playlists, use xhr-loader if not overrided by user config.
- [src/remux/dummy-remuxer.js][]
- example dummy remuxer
- [src/remux/mp4-generator.js][]
- in charge of generating MP4 boxes
- generate Init Segment (moov)
- generate samples Box (moof and mdat)
- [src/remux/mp4-remuxer.js][]
- in charge of converting AVC/AAC/MP3 samples provided by demuxer into fragmented ISO BMFF boxes, compatible with MediaSource
- this remuxer is able to deal with small gaps between fragments and ensure timestamp continuity. it is also able to create audio padding (silent AAC audio frames) in case there is a significant audio 'hole' in the stream.
- it notifies remuxing completion using events (`FRAG_PARSING_INIT_SEGMENT`, `FRAG_PARSING_DATA` and `FRAG_PARSED`)
- [src/utils/attr-list.js][]
- Attribute List parsing helper class, used by playlist-loader
- [src/utils/binary-search.js][]
- binary search helper class
- [src/utils/cea-608-parser.js][]
- Port of dash.js class of the same name to ECMAScript. This class outputs "Screen" objects which contain rows of characters that can be rendered by a separate class.
- [src/utils/cues.js][]
- Default CC renderer. Translates Screen objects from cea-608-parser into HTML5 VTTCue objects, rendered by the video tag
- [src/utils/ewma.js][]
- compute [exponential weighted moving average](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average)
- [src/utils/ewma-bandwidth-estimator.js][]
- Exponential Weighted Moving Average bandwidth estimator, heavily inspired from shaka-player
- Tracks bandwidth samples and estimates available bandwidth, based on the minimum of two exponentially-weighted moving averages with different half-lives.
- one fast average with a short half-life: useful to quickly react to bandwidth drop and switch rendition down quickly
- one slow average with a long half-life: useful to slowly react to bandwidth increase and avoid switching up rendition to quickly
- bandwidth estimate is Math.min(fast average,slow average)
- average half-life are configurable , refer to abrEwma\* config params
- [src/utils/hex.js][]
- Hex dump utils, useful for debug
- [src/utils/logger.js][]
- logging utils, useful for debug
- [src/utils/polyfill.js][]
- ArrayBuffer.slice polyfill
- [src/utils/url.js][]
- convert base+relative URL into absolute URL
- [src/utils/xhr-loader.js][]
- XmlHttpRequest wrapper. it handles standard HTTP GET but also retries and timeout.
- retries : if xhr fails, HTTP GET will be retried after a predetermined delay. this delay is increasing following an exponential backoff. after a predetemined max number of retries, an error callback will be triggered.
- timeout: if load exceeds max allowed duration, a timeout callback will be triggered. it is up to the callback to decides whether the connection should be cancelled or not.
[src/config.js]: ../src/config.js
[src/errors.js]: ../src/errors.js
@@ -269,37 +269,36 @@ design idea is pretty simple :
[src/utils/url.js]: ../src/utils/url.js
[src/utils/xhr-loader.js]: ../src/utils/xhr-loader.js
## Error detection and Handling
- ```MANIFEST_LOAD_ERROR``` is raised by [src/loader/playlist-loader.js][] upon xhr failure detected by [src/utils/xhr-loader.js][]. this error is marked as fatal only after manifestLoadingMaxRetry has been reached and will not be recovered automatically. a call to ```hls.loadSource(manifestURL)``` could help recover it.
- ```MANIFEST_LOAD_TIMEOUT``` is raised by [src/loader/playlist-loader.js][] upon xhr timeout detected by [src/utils/xhr-loader.js][]. this error is marked as fatal and will not be recovered automatically. a call to ```hls.loadSource(manifestURL)``` could help recover it.
- ```MANIFEST_PARSING_ERROR``` is raised by [src/loader/playlist-loader.js][] if Manifest parsing fails (no EXTM3U delimiter, no levels found in Manifest, ...)
- ```LEVEL_LOAD_ERROR``` is raised by [src/loader/playlist-loader.js][] upon xhr failure detected by [src/utils/xhr-loader.js][]. this error is marked as fatal only after levelLoadingMaxRetry has been reached and will not be recovered automatically. a call to ```hls.startLoad()``` could help recover it.
- ```LEVEL_LOAD_TIMEOUT``` is raised by [src/loader/playlist-loader.js][] upon xhr timeout detected by [src/utils/xhr-loader.js][]. this error is marked as fatal and will not be recovered automatically. a call to ```hls.startLoad()``` could help recover it.
- ```LEVEL_SWITCH_ERROR``` is raised by [src/controller/level-controller.js][] if user tries to switch to an invalid level (invalid/out of range level id)
- ```AUDIO_TRACK_LOAD_ERROR``` is raised by [src/loader/playlist-loader.js][] upon xhr failure detected by [src/utils/xhr-loader.js][]. this error is marked as fatal and will not be recovered automatically. a call to ```hls.startLoad()``` could help recover it.
- ```AUDIO_TRACK_LOAD_TIMEOUT``` is raised by [src/loader/playlist-loader.js][] upon xhr timeout detected by [src/utils/xhr-loader.js][]. this error is marked as fatal and will not be recovered automatically. a call to ```hls.startLoad()``` could help recover it.
- ```FRAG_LOAD_ERROR``` is raised by [src/loader/fragment-loader.js][] upon xhr failure detected by [src/utils/xhr-loader.js][].
- if auto level switch is enabled and loaded frag level is greater than 0, or if media.currentTime is buffered, this error is not fatal: in that case [src/controller/level-controller.js][] will trigger an emergency switch down to level 0.
- if frag level is 0 or auto level switch is disabled and media.currentTime is not buffered, this error is marked as fatal and a call to ```hls.startLoad()``` could help recover it.
- ```FRAG_LOAD_TIMEOUT``` is raised by [src/loader/fragment-loader.js][] upon xhr timeout detected by [src/utils/xhr-loader.js][].
- if auto level switch is enabled and loaded frag level is greater than 0, this error is not fatal: in that case [src/controller/level-controller.js][] will trigger an emergency switch down to level 0.
- if frag level is 0 or auto level switch is disabled, this error is marked as fatal and a call to ```hls.startLoad()``` could help recover it.
- ```FRAG_DECRYPT_ERROR``` is raised by [src/demux/demuxer.js][] upon fragment decrypting error. this error is fatal.
- ```FRAG_PARSING_ERROR``` is raised by [src/demux/tsdemuxer.js][] upon TS parsing error. this error is not fatal.
- ```REMUX_ALLOC_ERROR``` is raised by [src/remux/mp4-remuxer.js][] upon memory allocation error while remuxing. this error is not fatal if in auto-mode and loaded frag level is greater than 0. in that case a level switch down will occur.
- ```KEY_LOAD_ERROR``` is raised by [src/loader/key-loader.js][] upon xhr failure detected by [src/utils/xhr-loader.js][].
- if auto level switch is enabled and loaded frag level is greater than 0, this error is not fatal: in that case [src/controller/level-controller.js][] will trigger an emergency switch down to level 0.
- if frag level is 0 or auto level switch is disabled, this error is marked as fatal and a call to ```hls.startLoad()``` could help recover it.
- ```KEY_LOAD_TIMEOUT``` is raised by [src/loader/key-loader.js][] upon xhr timeout detected by [src/utils/xhr-loader.js][].
- if auto level switch is enabled and loaded frag level is greater than 0, this error is not fatal: in that case [src/controller/level-controller.js][] will trigger an emergency switch down to level 0.
- if frag level is 0 or auto level switch is disabled, this error is marked as fatal and a call to ```hls.startLoad()``` could help recover it.
- ```BUFFER_ADD_CODEC_ERROR``` is raised by [src/controller/buffer-controller.js][] when an exception is raised when calling mediaSource.addSourceBuffer(). this error is non fatal.
- ```BUFFER_APPEND_ERROR``` is raised by [src/controller/buffer-controller.js][] when an exception is raised when calling sourceBuffer.appendBuffer(). this error is non fatal and become fatal after config.appendErrorMaxRetry retries. when fatal, a call to ```hls.recoverMediaError()``` could help recover it.
- ```BUFFER_APPENDING_ERROR``` is raised by [src/controller/buffer-controller.js][] after SourceBuffer appending error. this error is fatal and a call to ```hls.recoverMediaError()``` could help recover it.
- ```BUFFER_STALLED_ERROR``` is raised by [src/controller/stream-controller.js][] if playback is stalling because of buffer underrun
- ```BUFFER_FULL_ERROR``` is raised by [src/controller/buffer-controller.js][] if sourcebuffer is full
- ```BUFFER_SEEK_OVER_HOLE``` is raised by [src/controller/stream-controller.js][] when hls.js seeks over a buffer hole after playback stalls
- ```BUFFER_NUDGE_ON_STALL``` is raised by [src/controller/stream-controller.js][] when hls.js nudge currentTime (when playback is stuck for more than 1s in a buffered area)
- ```INTERNAL_EXCEPTION``` is raised by [src/event-handler.js][] when a runtime exception is triggered by an internal Hls event handler. this error is non-fatal.
- `MANIFEST_LOAD_ERROR` is raised by [src/loader/playlist-loader.js][] upon xhr failure detected by [src/utils/xhr-loader.js][]. this error is marked as fatal only after manifestLoadingMaxRetry has been reached and will not be recovered automatically. a call to `hls.loadSource(manifestURL)` could help recover it.
- `MANIFEST_LOAD_TIMEOUT` is raised by [src/loader/playlist-loader.js][] upon xhr timeout detected by [src/utils/xhr-loader.js][]. this error is marked as fatal and will not be recovered automatically. a call to `hls.loadSource(manifestURL)` could help recover it.
- `MANIFEST_PARSING_ERROR` is raised by [src/loader/playlist-loader.js][] if Manifest parsing fails (no EXTM3U delimiter, no levels found in Manifest, ...)
- `LEVEL_LOAD_ERROR` is raised by [src/loader/playlist-loader.js][] upon xhr failure detected by [src/utils/xhr-loader.js][]. this error is marked as fatal only after levelLoadingMaxRetry has been reached and will not be recovered automatically. a call to `hls.startLoad()` could help recover it.
- `LEVEL_LOAD_TIMEOUT` is raised by [src/loader/playlist-loader.js][] upon xhr timeout detected by [src/utils/xhr-loader.js][]. this error is marked as fatal and will not be recovered automatically. a call to `hls.startLoad()` could help recover it.
- `LEVEL_SWITCH_ERROR` is raised by [src/controller/level-controller.js][] if user tries to switch to an invalid level (invalid/out of range level id)
- `AUDIO_TRACK_LOAD_ERROR` is raised by [src/loader/playlist-loader.js][] upon xhr failure detected by [src/utils/xhr-loader.js][]. this error is marked as fatal and will not be recovered automatically. a call to `hls.startLoad()` could help recover it.
- `AUDIO_TRACK_LOAD_TIMEOUT` is raised by [src/loader/playlist-loader.js][] upon xhr timeout detected by [src/utils/xhr-loader.js][]. this error is marked as fatal and will not be recovered automatically. a call to `hls.startLoad()` could help recover it.
- `FRAG_LOAD_ERROR` is raised by [src/loader/fragment-loader.js][] upon xhr failure detected by [src/utils/xhr-loader.js][].
- if auto level switch is enabled and loaded frag level is greater than 0, or if media.currentTime is buffered, this error is not fatal: in that case [src/controller/level-controller.js][] will trigger an emergency switch down to level 0.
- if frag level is 0 or auto level switch is disabled and media.currentTime is not buffered, this error is marked as fatal and a call to `hls.startLoad()` could help recover it.
- `FRAG_LOAD_TIMEOUT` is raised by [src/loader/fragment-loader.js][] upon xhr timeout detected by [src/utils/xhr-loader.js][].
- if auto level switch is enabled and loaded frag level is greater than 0, this error is not fatal: in that case [src/controller/level-controller.js][] will trigger an emergency switch down to level 0.
- if frag level is 0 or auto level switch is disabled, this error is marked as fatal and a call to `hls.startLoad()` could help recover it.
- `FRAG_DECRYPT_ERROR` is raised by [src/demux/demuxer.js][] upon fragment decrypting error. this error is fatal.
- `FRAG_PARSING_ERROR` is raised by [src/demux/tsdemuxer.js][] upon TS parsing error. this error is not fatal.
- `REMUX_ALLOC_ERROR` is raised by [src/remux/mp4-remuxer.js][] upon memory allocation error while remuxing. this error is not fatal if in auto-mode and loaded frag level is greater than 0. in that case a level switch down will occur.
- `KEY_LOAD_ERROR` is raised by [src/loader/key-loader.js][] upon xhr failure detected by [src/utils/xhr-loader.js][].
- if auto level switch is enabled and loaded frag level is greater than 0, this error is not fatal: in that case [src/controller/level-controller.js][] will trigger an emergency switch down to level 0.
- if frag level is 0 or auto level switch is disabled, this error is marked as fatal and a call to `hls.startLoad()` could help recover it.
- `KEY_LOAD_TIMEOUT` is raised by [src/loader/key-loader.js][] upon xhr timeout detected by [src/utils/xhr-loader.js][].
- if auto level switch is enabled and loaded frag level is greater than 0, this error is not fatal: in that case [src/controller/level-controller.js][] will trigger an emergency switch down to level 0.
- if frag level is 0 or auto level switch is disabled, this error is marked as fatal and a call to `hls.startLoad()` could help recover it.
- `BUFFER_ADD_CODEC_ERROR` is raised by [src/controller/buffer-controller.js][] when an exception is raised when calling mediaSource.addSourceBuffer(). this error is non fatal.
- `BUFFER_APPEND_ERROR` is raised by [src/controller/buffer-controller.js][] when an exception is raised when calling sourceBuffer.appendBuffer(). this error is non fatal and become fatal after config.appendErrorMaxRetry retries. when fatal, a call to `hls.recoverMediaError()` could help recover it.
- `BUFFER_APPENDING_ERROR` is raised by [src/controller/buffer-controller.js][] after SourceBuffer appending error. this error is fatal and a call to `hls.recoverMediaError()` could help recover it.
- `BUFFER_STALLED_ERROR` is raised by [src/controller/stream-controller.js][] if playback is stalling because of buffer underrun
- `BUFFER_FULL_ERROR` is raised by [src/controller/buffer-controller.js][] if sourcebuffer is full
- `BUFFER_SEEK_OVER_HOLE` is raised by [src/controller/stream-controller.js][] when hls.js seeks over a buffer hole after playback stalls
- `BUFFER_NUDGE_ON_STALL` is raised by [src/controller/stream-controller.js][] when hls.js nudge currentTime (when playback is stuck for more than 1s in a buffered area)
- `INTERNAL_EXCEPTION` is raised by [src/event-handler.js][] when a runtime exception is triggered by an internal Hls event handler. this error is non-fatal.
+3 -1
View File
@@ -1,7 +1,8 @@
# Performing A Release
Releases are performed automatically with [GitHub actions](https://github.com/video-dev/hls.js/actions?query=workflow%3ABuild+branch%3Amaster).
1. `git tag -a v<major>.<minor>.<patch>` or `git tag -a v<major>.<minor>.<patch>-<prerelease>` _('v' required)_ where anything before the first `.` in `<prerelease>` will be become the [npm dist-tag](https://docs.npmjs.com/cli/dist-tag).
1. `git tag -a v<major>.<minor>.<patch>` or `git tag -a v<major>.<minor>.<patch>-<prerelease>` _('v' required)_ where anything before the first `.` in `<prerelease>` will be become the [npm dist-tag](https://docs.npmjs.com/cli/dist-tag).
1. `git push`
1. `git push --tag`
1. Wait for the GitHub action to create a new draft GitHub release with the build attached. The publish to npm should happen around the same time from a different step.
@@ -9,6 +10,7 @@ Releases are performed automatically with [GitHub actions](https://github.com/vi
1. Publish the GitHub release.
## Examples
- `git tag -a v1.2.3` will result in `1.2.3` being published with the `latest` npm tag.
- `git tag -a v1.2.3-beta` will result in `1.2.3-beta` being published with the `beta` npm tag.
- `git tag -a v1.2.3-beta.1` will result in `1.2.3-beta.1` being published with the `beta` npm tag.
+3 -13
View File
@@ -1,19 +1,9 @@
{
"folders":
[
"folders": [
{
"path": ".",
"folder_exclude_patterns": [
".git",
"node_modules",
"dist",
"lib"
],
"file_exclude_patterns": [
".gitignore",
"hls.js.sublime-project",
"hls.js.sublime-workspace"
]
"folder_exclude_patterns": [".git", "node_modules", "dist", "lib"],
"file_exclude_patterns": [".gitignore", "hls.js.sublime-project", "hls.js.sublime-workspace"]
}
]
}
+17 -15
View File
@@ -14,16 +14,16 @@ const mergeConfig = merge(webpackConfig, {
{
loader: 'istanbul-instrumenter-loader',
options: {
esModules: true
}
}
]
}
]
esModules: true,
},
},
],
},
],
},
node: {
global: true
}
global: true,
},
});
module.exports = function (config) {
@@ -34,10 +34,12 @@ module.exports = function (config) {
// list of files / patterns to load in the browser
// https://github.com/webpack-contrib/karma-webpack#alternative-usage
files: [{
pattern: 'tests/index.js',
watched: false
}],
files: [
{
pattern: 'tests/index.js',
watched: false,
},
],
// list of files to exclude
exclude: [],
@@ -46,7 +48,7 @@ module.exports = function (config) {
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
// node_modules must not be webpacked or else Karma will fail to load frameworks
preprocessors: {
'tests/index.js': ['webpack', 'sourcemap']
'tests/index.js': ['webpack', 'sourcemap'],
},
// test results reporter to use
@@ -56,7 +58,7 @@ module.exports = function (config) {
coverageIstanbulReporter: {
reports: ['lcov', 'text-summary'],
fixWebpackSourcePaths: true
fixWebpackSourcePaths: true,
},
webpack: mergeConfig,
@@ -84,6 +86,6 @@ module.exports = function (config) {
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
concurrency: Infinity,
});
};
+5 -2
View File
@@ -8,7 +8,7 @@ try {
} else {
console.log('not published');
}
} catch(e) {
} catch (e) {
console.error(e);
process.exit(1);
}
@@ -16,5 +16,8 @@ process.exit(0);
function versionPublished() {
// npm view returns empty string if package doesn't exist
return !!require('child_process').execSync('npm view ' + packageJson.name + '@' + packageJson.version + ' --json').toString().trim();
return !!require('child_process')
.execSync('npm view ' + packageJson.name + '@' + packageJson.version + ' --json')
.toString()
.trim();
}
+14 -11
View File
@@ -1,19 +1,22 @@
#!/usr/bin/env node
const fs = require("fs"),
path = require("path");
const fs = require('fs'),
path = require('path');
var folderName = process.argv[2];
fs.readdir(folderName, function (err, files) {
if (err) {
throw err;
}
files.map(function (file) {
return path.join(folderName, file);
}).filter(function (file) {
return fs.statSync(file).isFile();
}).forEach(function (file) {
console.log("%s (%s)", file, fs.statSync(file).size);
if (err) {
throw err;
}
files
.map(function (file) {
return path.join(folderName, file);
})
.filter(function (file) {
return fs.statSync(file).isFile();
})
.forEach(function (file) {
console.log('%s (%s)', file, fs.statSync(file).size);
});
});
+9 -8
View File
@@ -39,10 +39,11 @@ try {
// remove v
intermediateVersion = intermediateVersion.substring(1);
const suffix = process.env.NETLIFY && process.env.CONTEXT === 'deploy-preview'
? `pr.${process.env.REVIEW_ID/* set by netlify */}.${getCommitHash().substr(0, 8)}`
: process.env.NETLIFY && process.env.CONTEXT === 'branch-deploy'
? `branch.${process.env.BRANCH/* set by netlify */.replace(/[^a-zA-Z0-9]/g, '-')}.${getCommitHash().substr(0, 8)}`
const suffix =
process.env.NETLIFY && process.env.CONTEXT === 'deploy-preview'
? `pr.${process.env.REVIEW_ID /* set by netlify */}.${getCommitHash().substr(0, 8)}`
: process.env.NETLIFY && process.env.CONTEXT === 'branch-deploy'
? `branch.${process.env.BRANCH /* set by netlify */.replace(/[^a-zA-Z0-9]/g, '-')}.${getCommitHash().substr(0, 8)}`
: `0.canary.${getCommitNum()}`;
newVersion = `${intermediateVersion}${isStable ? '-' : '.'}${suffix}`;
@@ -60,15 +61,15 @@ try {
}
process.exit(0);
function getCommitNum () {
function getCommitNum() {
return parseInt(exec('git rev-list --count HEAD'), 10);
}
function getCommitHash () {
function getCommitHash() {
return exec('git rev-parse HEAD');
}
function getLatestVersionTag () {
function getLatestVersionTag() {
let commitish = '';
while (true) {
const tag = exec('git describe --abbrev=0 --match="v*" ' + commitish);
@@ -83,6 +84,6 @@ function getLatestVersionTag () {
}
}
function exec (cmd) {
function exec(cmd) {
return require('child_process').execSync(cmd).toString().trim();
}
+1 -1
View File
@@ -55,5 +55,5 @@ module.exports = {
throw new Error('Invalid version.');
}
return match[1] || 'latest';
}
},
};
+114 -113
View File
@@ -18,151 +18,149 @@ import type { MediaKeyFunc } from './utils/mediakeys-helper';
import type { FragmentLoaderContext, Loader, LoaderContext, PlaylistLoaderContext } from './types/loader';
type ABRControllerConfig = {
abrEwmaFastLive: number,
abrEwmaSlowLive: number,
abrEwmaFastVoD: number,
abrEwmaSlowVoD: number,
abrEwmaDefaultEstimate: number,
abrBandWidthFactor: number,
abrBandWidthUpFactor: number,
abrMaxWithRealBitrate: boolean,
maxStarvationDelay: number,
maxLoadingDelay: number,
abrEwmaFastLive: number;
abrEwmaSlowLive: number;
abrEwmaFastVoD: number;
abrEwmaSlowVoD: number;
abrEwmaDefaultEstimate: number;
abrBandWidthFactor: number;
abrBandWidthUpFactor: number;
abrMaxWithRealBitrate: boolean;
maxStarvationDelay: number;
maxLoadingDelay: number;
};
export type BufferControllerConfig = {
appendErrorMaxRetry: number,
liveDurationInfinity: boolean,
liveBackBufferLength: number,
appendErrorMaxRetry: number;
liveDurationInfinity: boolean;
liveBackBufferLength: number;
};
type CapLevelControllerConfig = {
capLevelToPlayerSize: boolean
capLevelToPlayerSize: boolean;
};
export type DRMSystemOptions = {
audioRobustness?: string,
videoRobustness?: string,
}
audioRobustness?: string;
videoRobustness?: string;
};
export type EMEControllerConfig = {
licenseXhrSetup?: (xhr: XMLHttpRequest, url: string) => void,
emeEnabled: boolean,
widevineLicenseUrl?: string,
drmSystemOptions: DRMSystemOptions,
requestMediaKeySystemAccessFunc: MediaKeyFunc | null,
licenseXhrSetup?: (xhr: XMLHttpRequest, url: string) => void;
emeEnabled: boolean;
widevineLicenseUrl?: string;
drmSystemOptions: DRMSystemOptions;
requestMediaKeySystemAccessFunc: MediaKeyFunc | null;
};
type FragmentLoaderConfig = {
fLoader?: { new(confg: HlsConfig): Loader<FragmentLoaderContext> },
fLoader?: { new (confg: HlsConfig): Loader<FragmentLoaderContext> };
fragLoadingTimeOut: number,
fragLoadingMaxRetry: number,
fragLoadingRetryDelay: number,
fragLoadingMaxRetryTimeout: number,
fragLoadingTimeOut: number;
fragLoadingMaxRetry: number;
fragLoadingRetryDelay: number;
fragLoadingMaxRetryTimeout: number;
};
type FPSControllerConfig = {
capLevelOnFPSDrop: boolean,
fpsDroppedMonitoringPeriod: number,
fpsDroppedMonitoringThreshold: number,
capLevelOnFPSDrop: boolean;
fpsDroppedMonitoringPeriod: number;
fpsDroppedMonitoringThreshold: number;
};
type LevelControllerConfig = {
startLevel?: number
startLevel?: number;
};
export type MP4RemuxerConfig = {
stretchShortVideoTrack: boolean,
maxAudioFramesDrift: number,
stretchShortVideoTrack: boolean;
maxAudioFramesDrift: number;
};
type PlaylistLoaderConfig = {
pLoader?: { new(confg: HlsConfig): Loader<PlaylistLoaderContext> },
pLoader?: { new (confg: HlsConfig): Loader<PlaylistLoaderContext> };
manifestLoadingTimeOut: number,
manifestLoadingMaxRetry: number,
manifestLoadingRetryDelay: number,
manifestLoadingMaxRetryTimeout: number,
manifestLoadingTimeOut: number;
manifestLoadingMaxRetry: number;
manifestLoadingRetryDelay: number;
manifestLoadingMaxRetryTimeout: number;
levelLoadingTimeOut: number,
levelLoadingMaxRetry: number,
levelLoadingRetryDelay: number,
levelLoadingMaxRetryTimeout: number
levelLoadingTimeOut: number;
levelLoadingMaxRetry: number;
levelLoadingRetryDelay: number;
levelLoadingMaxRetryTimeout: number;
};
type StreamControllerConfig = {
autoStartLoad: boolean,
startPosition: number,
defaultAudioCodec?: string,
initialLiveManifestSize: number,
maxBufferLength: number,
maxBufferSize: number,
maxBufferHole: number,
highBufferWatchdogPeriod: number,
nudgeOffset: number,
nudgeMaxRetry: number,
maxFragLookUpTolerance: number,
maxMaxBufferLength: number,
startFragPrefetch: boolean,
testBandwidth: boolean
autoStartLoad: boolean;
startPosition: number;
defaultAudioCodec?: string;
initialLiveManifestSize: number;
maxBufferLength: number;
maxBufferSize: number;
maxBufferHole: number;
highBufferWatchdogPeriod: number;
nudgeOffset: number;
nudgeMaxRetry: number;
maxFragLookUpTolerance: number;
maxMaxBufferLength: number;
startFragPrefetch: boolean;
testBandwidth: boolean;
};
type LatencyControllerConfig = {
liveSyncDurationCount: number,
liveMaxLatencyDurationCount: number,
liveSyncDuration?: number,
liveMaxLatencyDuration?: number,
maxLiveSyncPlaybackRate: number
}
liveSyncDurationCount: number;
liveMaxLatencyDurationCount: number;
liveSyncDuration?: number;
liveMaxLatencyDuration?: number;
maxLiveSyncPlaybackRate: number;
};
type TimelineControllerConfig = {
cueHandler: Cues.CuesInterface,
enableCEA708Captions: boolean,
enableWebVTT: boolean,
enableIMSC1: boolean,
captionsTextTrack1Label: string,
captionsTextTrack1LanguageCode: string,
captionsTextTrack2Label: string,
captionsTextTrack2LanguageCode: string,
captionsTextTrack3Label: string,
captionsTextTrack3LanguageCode: string,
captionsTextTrack4Label: string,
captionsTextTrack4LanguageCode: string,
renderTextTracksNatively: boolean
cueHandler: Cues.CuesInterface;
enableCEA708Captions: boolean;
enableWebVTT: boolean;
enableIMSC1: boolean;
captionsTextTrack1Label: string;
captionsTextTrack1LanguageCode: string;
captionsTextTrack2Label: string;
captionsTextTrack2LanguageCode: string;
captionsTextTrack3Label: string;
captionsTextTrack3LanguageCode: string;
captionsTextTrack4Label: string;
captionsTextTrack4LanguageCode: string;
renderTextTracksNatively: boolean;
};
type TSDemuxerConfig = {
forceKeyFrameOnDiscontinuity: boolean,
forceKeyFrameOnDiscontinuity: boolean;
};
export type HlsConfig =
{
debug: boolean,
enableWorker: boolean,
enableSoftwareAES: boolean,
minAutoBitrate: number,
loader: { new(confg: HlsConfig): Loader<LoaderContext> },
xhrSetup?: (xhr: XMLHttpRequest, url: string) => void,
export type HlsConfig = {
debug: boolean;
enableWorker: boolean;
enableSoftwareAES: boolean;
minAutoBitrate: number;
loader: { new (confg: HlsConfig): Loader<LoaderContext> };
xhrSetup?: (xhr: XMLHttpRequest, url: string) => void;
// Alt Audio
audioStreamController?: typeof AudioStreamController,
audioTrackController?: typeof AudioTrackController,
// Subtitle
subtitleStreamController?: typeof SubtitleStreamController,
subtitleTrackController?: typeof SubtitleTrackController,
timelineController?: typeof TimelineController,
// EME
emeController?: typeof EMEController,
// Alt Audio
audioStreamController?: typeof AudioStreamController;
audioTrackController?: typeof AudioTrackController;
// Subtitle
subtitleStreamController?: typeof SubtitleStreamController;
subtitleTrackController?: typeof SubtitleTrackController;
timelineController?: typeof TimelineController;
// EME
emeController?: typeof EMEController;
abrController: typeof AbrController,
bufferController: typeof BufferController,
capLevelController: typeof CapLevelController,
fpsController: typeof FPSController,
progressive: boolean,
lowLatencyMode: boolean
} &
ABRControllerConfig &
abrController: typeof AbrController;
bufferController: typeof BufferController;
capLevelController: typeof CapLevelController;
fpsController: typeof FPSController;
progressive: boolean;
lowLatencyMode: boolean;
} & ABRControllerConfig &
BufferControllerConfig &
CapLevelControllerConfig &
EMEControllerConfig &
@@ -255,15 +253,15 @@ export const hlsDefaultConfig: HlsConfig = {
// Dynamic Modules
...timelineConfig(),
subtitleStreamController: (__USE_SUBTITLES__) ? SubtitleStreamController : undefined,
subtitleTrackController: (__USE_SUBTITLES__) ? SubtitleTrackController : undefined,
timelineController: (__USE_SUBTITLES__) ? TimelineController : undefined,
audioStreamController: (__USE_ALT_AUDIO__) ? AudioStreamController : undefined,
audioTrackController: (__USE_ALT_AUDIO__) ? AudioTrackController : undefined,
emeController: (__USE_EME_DRM__) ? EMEController : undefined
subtitleStreamController: __USE_SUBTITLES__ ? SubtitleStreamController : undefined,
subtitleTrackController: __USE_SUBTITLES__ ? SubtitleTrackController : undefined,
timelineController: __USE_SUBTITLES__ ? TimelineController : undefined,
audioStreamController: __USE_ALT_AUDIO__ ? AudioStreamController : undefined,
audioTrackController: __USE_ALT_AUDIO__ ? AudioTrackController : undefined,
emeController: __USE_EME_DRM__ ? EMEController : undefined,
};
function timelineConfig (): TimelineControllerConfig {
function timelineConfig(): TimelineControllerConfig {
return {
cueHandler: Cues, // used by timeline-controller
enableCEA708Captions: __USE_SUBTITLES__, // used by timeline-controller
@@ -277,16 +275,19 @@ function timelineConfig (): TimelineControllerConfig {
captionsTextTrack3LanguageCode: '', // used by timeline-controller
captionsTextTrack4Label: 'Unknown CC', // used by timeline-controller
captionsTextTrack4LanguageCode: '', // used by timeline-controller
renderTextTracksNatively: true
renderTextTracksNatively: true,
};
}
export function mergeConfig (defaultConfig: HlsConfig, userConfig: Partial<HlsConfig>): HlsConfig {
export function mergeConfig(defaultConfig: HlsConfig, userConfig: Partial<HlsConfig>): HlsConfig {
if ((userConfig.liveSyncDurationCount || userConfig.liveMaxLatencyDurationCount) && (userConfig.liveSyncDuration || userConfig.liveMaxLatencyDuration)) {
throw new Error('Illegal hls.js config: don\'t mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration');
throw new Error("Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration");
}
if (userConfig.liveMaxLatencyDurationCount !== undefined && (userConfig.liveSyncDurationCount === undefined || userConfig.liveMaxLatencyDurationCount <= userConfig.liveSyncDurationCount)) {
if (
userConfig.liveMaxLatencyDurationCount !== undefined &&
(userConfig.liveSyncDurationCount === undefined || userConfig.liveMaxLatencyDurationCount <= userConfig.liveSyncDurationCount)
) {
throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be greater than "liveSyncDurationCount"');
}
@@ -297,7 +298,7 @@ export function mergeConfig (defaultConfig: HlsConfig, userConfig: Partial<HlsCo
return Object.assign({}, defaultConfig, userConfig);
}
export function enableStreamingMode (config) {
export function enableStreamingMode(config) {
const currentLoader = config.loader;
if (currentLoader !== FetchLoader && currentLoader !== XhrLoader) {
// If a developer has configured their own loader, respect that choice
+43 -39
View File
@@ -32,7 +32,7 @@ class AbrController implements ComponentAPI {
public readonly bwEstimator: EwmaBandWidthEstimator;
constructor (hls: Hls) {
constructor(hls: Hls) {
this.hls = hls;
const config = hls.config;
@@ -41,7 +41,7 @@ class AbrController implements ComponentAPI {
this.registerListeners();
}
protected registerListeners () {
protected registerListeners() {
const { hls } = this;
hls.on(Events.FRAG_LOADING, this.onFragLoading, this);
hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
@@ -50,7 +50,7 @@ class AbrController implements ComponentAPI {
hls.on(Events.ERROR, this.onError, this);
}
protected unregisterListeners () {
protected unregisterListeners() {
const { hls } = this;
hls.off(Events.FRAG_LOADING, this.onFragLoading, this);
hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
@@ -59,12 +59,12 @@ class AbrController implements ComponentAPI {
hls.off(Events.ERROR, this.onError, this);
}
public destroy () {
public destroy() {
this.unregisterListeners();
this.clearTimer();
}
protected onFragLoading (event: Events.FRAG_LOADING, data: FragLoadingData) {
protected onFragLoading(event: Events.FRAG_LOADING, data: FragLoadingData) {
const frag = data.frag;
if (frag.type === 'main') {
if (!this.timer) {
@@ -75,7 +75,7 @@ class AbrController implements ComponentAPI {
}
}
protected onLevelLoaded (event: Events.LEVEL_LOADED, data: LevelLoadedData) {
protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
const config = this.hls.config;
if (data.details.live) {
this.bwEstimator.update(config.abrEwmaSlowLive, config.abrEwmaFastLive);
@@ -88,7 +88,7 @@ class AbrController implements ComponentAPI {
This method monitors the download rate of the current fragment, and will downswitch if that fragment will not load
quickly enough to prevent underbuffering
*/
private _abandonRulesCheck () {
private _abandonRulesCheck() {
const { fragCurrent: frag, partCurrent: part, hls } = this;
const { autoLevelEnabled, config, media } = hls;
if (!frag || !media) {
@@ -114,14 +114,14 @@ class AbrController implements ComponentAPI {
const requestDelay = performance.now() - stats.loading.start;
const playbackRate = Math.abs(media.playbackRate);
// In order to work with a stable bandwidth, only begin monitoring bandwidth after half of the fragment has been loaded
if (requestDelay <= (500 * duration / playbackRate)) {
if (requestDelay <= (500 * duration) / playbackRate) {
return;
}
const { levels, minAutoLevel } = hls;
const level = levels[frag.level];
const expectedLen = stats.total || Math.max(stats.loaded, Math.round(duration * level.maxBitrate / 8));
const loadRate = Math.max(1, stats.bwEstimate ? (stats.bwEstimate / 8) : (stats.loaded * 1000 / requestDelay));
const expectedLen = stats.total || Math.max(stats.loaded, Math.round((duration * level.maxBitrate) / 8));
const loadRate = Math.max(1, stats.bwEstimate ? stats.bwEstimate / 8 : (stats.loaded * 1000) / requestDelay);
// fragLoadDelay is an estimate of the time (in seconds) it will take to buffer the entire fragment
const fragLoadedDelay = (expectedLen - stats.loaded) / loadRate;
@@ -131,7 +131,7 @@ class AbrController implements ComponentAPI {
// Attempt an emergency downswitch only if less than 2 fragment lengths are buffered, and the time to finish loading
// the current fragment is greater than the amount of buffer we have left
if ((bufferStarvationDelay >= (2 * duration / playbackRate)) || (fragLoadedDelay <= bufferStarvationDelay)) {
if (bufferStarvationDelay >= (2 * duration) / playbackRate || fragLoadedDelay <= bufferStarvationDelay) {
return;
}
@@ -143,7 +143,7 @@ class AbrController implements ComponentAPI {
// 0.8 : consider only 80% of current bw to be conservative
// 8 = bits per byte (bps/Bps)
const levelNextBitrate = levels[nextLoadLevel].maxBitrate;
fragLevelNextLoadedDelay = duration * levelNextBitrate / (8 * 0.8 * loadRate);
fragLevelNextLoadedDelay = (duration * levelNextBitrate) / (8 * 0.8 * loadRate);
if (fragLevelNextLoadedDelay < bufferStarvationDelay) {
break;
@@ -172,7 +172,7 @@ class AbrController implements ComponentAPI {
hls.trigger(Events.FRAG_LOAD_EMERGENCY_ABORTED, { frag, part, stats });
}
protected onFragLoaded (event: Events.FRAG_LOADED, { frag, part }: FragLoadedData) {
protected onFragLoaded(event: Events.FRAG_LOADED, { frag, part }: FragLoadedData) {
if (frag.type === 'main' && Number.isFinite(frag.sn as number)) {
const stats = part ? part.stats : frag.stats;
const duration = part ? part.duration : frag.duration;
@@ -189,21 +189,21 @@ class AbrController implements ComponentAPI {
const loadedBytes = (level.loaded ? level.loaded.bytes : 0) + stats.loaded;
const loadedDuration = (level.loaded ? level.loaded.duration : 0) + duration;
level.loaded = { bytes: loadedBytes, duration: loadedDuration };
level.realBitrate = Math.round(8 * loadedBytes / loadedDuration);
level.realBitrate = Math.round((8 * loadedBytes) / loadedDuration);
}
if (frag.bitrateTest) {
const fragBufferedData: FragBufferedData = {
stats,
frag,
part,
id: frag.type
id: frag.type,
};
this.onFragBuffered(Events.FRAG_BUFFERED, fragBufferedData);
}
}
}
protected onFragBuffered (event: Events.FRAG_BUFFERED, data: FragBufferedData) {
protected onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
const { frag, part } = data;
const stats = part ? part.stats : frag.stats;
@@ -227,25 +227,25 @@ class AbrController implements ComponentAPI {
}
}
protected onError (event: Events.ERROR, data: ErrorData) {
protected onError(event: Events.ERROR, data: ErrorData) {
// stop timer in case of frag loading error
switch (data.details) {
case ErrorDetails.FRAG_LOAD_ERROR:
case ErrorDetails.FRAG_LOAD_TIMEOUT:
this.clearTimer();
break;
default:
break;
case ErrorDetails.FRAG_LOAD_ERROR:
case ErrorDetails.FRAG_LOAD_TIMEOUT:
this.clearTimer();
break;
default:
break;
}
}
clearTimer () {
clearTimer() {
self.clearInterval(this.timer);
this.timer = undefined;
}
// return next auto level
get nextAutoLevel () {
get nextAutoLevel() {
const forcedAutoLevel = this._nextAutoLevel;
const bwEstimator = this.bwEstimator;
// in case next auto level has been forced, and bw not available or not reliable, return forced value
@@ -263,15 +263,15 @@ class AbrController implements ComponentAPI {
return nextABRAutoLevel;
}
get _nextABRAutoLevel () {
get _nextABRAutoLevel() {
const { fragCurrent, partCurrent, hls } = this;
const { maxAutoLevel, config, minAutoLevel, media } = hls;
const currentFragDuration = partCurrent ? partCurrent.duration : (fragCurrent ? fragCurrent.duration : 0);
const pos = (media ? media.currentTime : 0);
const currentFragDuration = partCurrent ? partCurrent.duration : fragCurrent ? fragCurrent.duration : 0;
const pos = media ? media.currentTime : 0;
// playbackRate is the absolute value of the playback rate; if media.playbackRate is 0, we use 1 to load as
// if we're playing back at the normal rate.
const playbackRate = ((media && (media.playbackRate !== 0)) ? Math.abs(media.playbackRate) : 1.0);
const playbackRate = media && media.playbackRate !== 0 ? Math.abs(media.playbackRate) : 1.0;
const avgbw = this.bwEstimator ? this.bwEstimator.getEstimate() : config.abrEwmaDefaultEstimate;
// bufferStarvationDelay is the wall-clock time left until the playback buffer is exhausted.
const bufferStarvationDelay = (BufferHelper.bufferInfo(media as Bufferable, pos, config.maxBufferHole).end - pos) / playbackRate;
@@ -309,11 +309,11 @@ class AbrController implements ComponentAPI {
}
}
private _findBestLevel (currentBw: number, minAutoLevel: number, maxAutoLevel: number, maxFetchDuration: number, bwFactor: number, bwUpFactor: number): number {
private _findBestLevel(currentBw: number, minAutoLevel: number, maxAutoLevel: number, maxFetchDuration: number, bwFactor: number, bwUpFactor: number): number {
const { fragCurrent, partCurrent, lastLoadedFragLevel: currentLevel } = this;
const { levels } = this.hls;
const live = levels[currentLevel]?.details?.live || false;
const currentFragDuration = partCurrent ? partCurrent.duration : (fragCurrent ? fragCurrent.duration : 0);
const currentFragDuration = partCurrent ? partCurrent.duration : fragCurrent ? fragCurrent.duration : 0;
for (let i = maxAutoLevel; i >= minAutoLevel; i--) {
const levelInfo = levels[i];
@@ -338,15 +338,19 @@ class AbrController implements ComponentAPI {
}
const bitrate: number = levels[i].maxBitrate;
const fetchDuration: number = bitrate * avgDuration / adjustedbw;
const fetchDuration: number = (bitrate * avgDuration) / adjustedbw;
logger.trace(`level/adjustedbw/bitrate/avgDuration/maxFetchDuration/fetchDuration: ${i}/${Math.round(adjustedbw)}/${bitrate}/${avgDuration}/${maxFetchDuration}/${fetchDuration}`);
logger.trace(
`level/adjustedbw/bitrate/avgDuration/maxFetchDuration/fetchDuration: ${i}/${Math.round(adjustedbw)}/${bitrate}/${avgDuration}/${maxFetchDuration}/${fetchDuration}`
);
// if adjusted bw is greater than level bitrate AND
if (adjustedbw > bitrate &&
// fragment fetchDuration unknown OR live stream OR fragment fetchDuration less than max allowed fetch duration, then this level matches
// we don't account for max Fetch Duration for live streams, this is to avoid switching down when near the edge of live sliding window ...
// special case to support startLevel = -1 (bitrateTest) on live streams : in that case we should not exit loop so that _findBestLevel will return -1
(!fetchDuration || (live && !this.bitrateTestDelay) || fetchDuration < maxFetchDuration)) {
if (
adjustedbw > bitrate &&
// fragment fetchDuration unknown OR live stream OR fragment fetchDuration less than max allowed fetch duration, then this level matches
// we don't account for max Fetch Duration for live streams, this is to avoid switching down when near the edge of live sliding window ...
// special case to support startLevel = -1 (bitrateTest) on live streams : in that case we should not exit loop so that _findBestLevel will return -1
(!fetchDuration || (live && !this.bitrateTestDelay) || fetchDuration < maxFetchDuration)
) {
// as we are looping from highest to lowest, this will return the best achievable quality level
return i;
}
@@ -355,7 +359,7 @@ class AbrController implements ComponentAPI {
return -1;
}
set nextAutoLevel (nextLevel) {
set nextAutoLevel(nextLevel) {
this._nextAutoLevel = nextLevel;
}
}
+177 -170
View File
@@ -32,16 +32,16 @@ import type {
FragLoadedData,
FragParsingMetadataData,
FragParsingUserdataData,
FragBufferedData
FragBufferedData,
} from '../types/events';
const TICK_INTERVAL = 100; // how often to tick in ms
type WaitingForPTSData = {
frag: Fragment,
part: Part | null,
cache: ChunkCache,
complete: boolean
frag: Fragment;
part: Part | null;
cache: ChunkCache;
complete: boolean;
};
class AudioStreamController extends BaseStreamController implements NetworkComponentAPI {
@@ -54,18 +54,18 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
private waitingData: WaitingForPTSData | null = null;
private mainDetails: LevelDetails | null = null;
constructor (hls: Hls, fragmentTracker: FragmentTracker) {
constructor(hls: Hls, fragmentTracker: FragmentTracker) {
super(hls, fragmentTracker, '[audio-stream-controller]');
this.fragmentLoader = new FragmentLoader(hls.config);
this._registerListeners();
}
protected onHandlerDestroying () {
protected onHandlerDestroying() {
this._unregisterListeners();
}
private _registerListeners () {
private _registerListeners() {
const { hls } = this;
hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -82,7 +82,7 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
}
private _unregisterListeners () {
private _unregisterListeners() {
const { hls } = this;
hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -100,7 +100,7 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
}
// INIT_PTS_FOUND is triggered when the video track parsed in the stream-controller has a new PTS value
onInitPtsFound (event: Events.INIT_PTS_FOUND, { frag, id, initPTS }: InitPTSFoundData) {
onInitPtsFound(event: Events.INIT_PTS_FOUND, { frag, id, initPTS }: InitPTSFoundData) {
// Always update the new INIT PTS
// Can change due level switch
if (id === 'main') {
@@ -115,7 +115,7 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
}
}
startLoad (startPosition) {
startLoad(startPosition) {
if (!this.levels) {
this.startPosition = startPosition;
this.state = State.STOPPED;
@@ -137,74 +137,74 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
this.tick();
}
doTick () {
doTick() {
switch (this.state) {
case State.IDLE:
this.doTickIdle();
break;
case State.WAITING_TRACK: {
const { levels, trackId } = this;
const details = levels?.[trackId]?.details;
if (details) {
if (this.waitForCdnTuneIn(details)) {
break;
}
this.state = State.WAITING_INIT_PTS;
}
break;
}
case State.FRAG_LOADING_WAITING_RETRY: {
const now = performance.now();
const retryDate = this.retryDate;
// if current time is gt than retryDate, or if media seeking let's switch to IDLE state to retry loading
if (!retryDate || (now >= retryDate) || this.media?.seeking) {
this.log('RetryDate reached, switch back to IDLE state');
this.state = State.IDLE;
}
break;
}
case State.WAITING_INIT_PTS: {
// Ensure we don't get stuck in the WAITING_INIT_PTS state if the waiting frag CC doesn't match any initPTS
const waitingData = this.waitingData;
if (waitingData) {
const { frag, part, cache, complete } = waitingData;
if (this.initPTS[frag.cc] !== undefined) {
this.waitingData = null;
this.state = State.FRAG_LOADING;
const payload = cache.flush();
const data: FragLoadedData = {
frag,
part,
payload,
networkDetails: null
};
this._handleFragmentLoadProgress(data);
if (complete) {
super._handleFragmentLoadComplete(data);
case State.IDLE:
this.doTickIdle();
break;
case State.WAITING_TRACK: {
const { levels, trackId } = this;
const details = levels?.[trackId]?.details;
if (details) {
if (this.waitForCdnTuneIn(details)) {
break;
}
} else if (this.videoTrackCC !== this.waitingVideoCC) {
// Drop waiting fragment if videoTrackCC has changed since waitingFragment was set and initPTS was not found
logger.log(`Waiting fragment cc (${frag.cc}) cancelled because video is at cc ${this.videoTrackCC}`);
this.clearWaitingFragment();
} else {
// Drop waiting fragment if an earlier fragment is needed
const bufferInfo = BufferHelper.bufferInfo(this.mediaBuffer, this.media.currentTime, this.config.maxBufferHole);
const waitingFragmentAtPosition = fragmentWithinToleranceTest(bufferInfo.end, this.config.maxFragLookUpTolerance, frag);
if (waitingFragmentAtPosition < 0) {
logger.log(`Waiting fragment cc (${frag.cc}) @ ${frag.start} cancelled because another fragment at ${bufferInfo.end} is needed`);
this.state = State.WAITING_INIT_PTS;
}
break;
}
case State.FRAG_LOADING_WAITING_RETRY: {
const now = performance.now();
const retryDate = this.retryDate;
// if current time is gt than retryDate, or if media seeking let's switch to IDLE state to retry loading
if (!retryDate || now >= retryDate || this.media?.seeking) {
this.log('RetryDate reached, switch back to IDLE state');
this.state = State.IDLE;
}
break;
}
case State.WAITING_INIT_PTS: {
// Ensure we don't get stuck in the WAITING_INIT_PTS state if the waiting frag CC doesn't match any initPTS
const waitingData = this.waitingData;
if (waitingData) {
const { frag, part, cache, complete } = waitingData;
if (this.initPTS[frag.cc] !== undefined) {
this.waitingData = null;
this.state = State.FRAG_LOADING;
const payload = cache.flush();
const data: FragLoadedData = {
frag,
part,
payload,
networkDetails: null,
};
this._handleFragmentLoadProgress(data);
if (complete) {
super._handleFragmentLoadComplete(data);
}
} else if (this.videoTrackCC !== this.waitingVideoCC) {
// Drop waiting fragment if videoTrackCC has changed since waitingFragment was set and initPTS was not found
logger.log(`Waiting fragment cc (${frag.cc}) cancelled because video is at cc ${this.videoTrackCC}`);
this.clearWaitingFragment();
} else {
// Drop waiting fragment if an earlier fragment is needed
const bufferInfo = BufferHelper.bufferInfo(this.mediaBuffer, this.media.currentTime, this.config.maxBufferHole);
const waitingFragmentAtPosition = fragmentWithinToleranceTest(bufferInfo.end, this.config.maxFragLookUpTolerance, frag);
if (waitingFragmentAtPosition < 0) {
logger.log(`Waiting fragment cc (${frag.cc}) @ ${frag.start} cancelled because another fragment at ${bufferInfo.end} is needed`);
this.clearWaitingFragment();
}
}
} else {
this.state = State.IDLE;
}
} else {
this.state = State.IDLE;
}
}
}
this.onTickEnd();
}
clearWaitingFragment () {
clearWaitingFragment() {
const waitingData = this.waitingData;
if (waitingData) {
this.fragmentTracker.removeFragment(waitingData.frag);
@@ -214,7 +214,7 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
}
}
protected onTickEnd () {
protected onTickEnd() {
const { media } = this;
if (!media || !media.readyState) {
// Exit early if we don't have media or if the media hasn't buffered anything yet (readyState 0)
@@ -230,7 +230,7 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
this.lastCurrentTime = media.currentTime;
}
private doTickIdle () {
private doTickIdle() {
const { hls, levels, media, trackId } = this;
const config = hls.config;
@@ -317,17 +317,17 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
}
}
onMediaDetaching () {
onMediaDetaching() {
this.videoBuffer = null;
super.onMediaDetaching();
}
onAudioTracksUpdated (event: Events.AUDIO_TRACKS_UPDATED, { audioTracks }: AudioTracksUpdatedData) {
onAudioTracksUpdated(event: Events.AUDIO_TRACKS_UPDATED, { audioTracks }: AudioTracksUpdatedData) {
this.log('Audio tracks updated');
this.levels = audioTracks.map(mediaPlaylist => new Level(mediaPlaylist));
this.levels = audioTracks.map((mediaPlaylist) => new Level(mediaPlaylist));
}
onAudioTrackSwitching (event: Events.AUDIO_TRACK_SWITCHING, data: AudioTrackSwitchingData) {
onAudioTrackSwitching(event: Events.AUDIO_TRACK_SWITCHING, data: AudioTrackSwitchingData) {
// if any URL found on new audio track, it is an alternate audio track
const altAudio = !!data.url;
this.trackId = data.id;
@@ -360,19 +360,19 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
this.tick();
}
onManifestLoading () {
onManifestLoading() {
this.mainDetails = null;
this.fragmentTracker.removeAllFragments();
this.startPosition = this.lastCurrentTime = 0;
}
onLevelLoaded (event: Events.LEVEL_LOADED, data: LevelLoadedData) {
onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
if (this.mainDetails === null) {
this.mainDetails = data.details;
}
}
onAudioTrackLoaded (event: Events.AUDIO_TRACK_LOADED, data: TrackLoadedData) {
onAudioTrackLoaded(event: Events.AUDIO_TRACK_LOADED, data: TrackLoadedData) {
const { levels } = this;
const { details: newDetails, id: trackId } = data;
if (!levels) {
@@ -413,7 +413,7 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
this.tick();
}
_handleFragmentLoadProgress (data: FragLoadedData) {
_handleFragmentLoadProgress(data: FragLoadedData) {
const { frag, part, payload } = data;
const { config, trackId, levels } = this;
if (!levels) {
@@ -429,8 +429,7 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
let transmuxer = this.transmuxer;
if (!transmuxer) {
transmuxer = this.transmuxer =
new TransmuxerInterface(this.hls, PlaylistLevelType.AUDIO, this._handleTransmuxComplete.bind(this), this._handleTransmuxerFlush.bind(this));
transmuxer = this.transmuxer = new TransmuxerInterface(this.hls, PlaylistLevelType.AUDIO, this._handleTransmuxComplete.bind(this), this._handleTransmuxerFlush.bind(this));
}
// Check if we have video initPTS
@@ -447,27 +446,27 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
transmuxer.push(payload, initSegmentData, audioCodec, '', frag, part, details.totalduration, accurateTimeOffset, chunkMeta, initPTS);
} else {
logger.log(`Unknown video PTS for cc ${frag.cc}, waiting for video PTS before demuxing audio frag ${frag.sn} of [${details.startSN} ,${details.endSN}],track ${trackId}`);
const { cache } = this.waitingData = this.waitingData || { frag, part, cache: new ChunkCache(), complete: false };
const { cache } = (this.waitingData = this.waitingData || { frag, part, cache: new ChunkCache(), complete: false });
cache.push(new Uint8Array(payload));
this.waitingVideoCC = this.videoTrackCC;
this.state = State.WAITING_INIT_PTS;
}
}
protected _handleFragmentLoadComplete (fragLoadedData: FragLoadedData) {
protected _handleFragmentLoadComplete(fragLoadedData: FragLoadedData) {
if (this.waitingData) {
return;
}
super._handleFragmentLoadComplete(fragLoadedData);
}
onBufferReset () {
onBufferReset() {
// reset reference to sourcebuffers
this.mediaBuffer = this.videoBuffer = null;
this.loadedmetadata = false;
}
onBufferCreated (event: Events.BUFFER_CREATED, data: BufferCreatedData) {
onBufferCreated(event: Events.BUFFER_CREATED, data: BufferCreatedData) {
const audioTrack = data.tracks.audio;
if (audioTrack) {
this.mediaBuffer = audioTrack.buffer;
@@ -477,7 +476,7 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
}
}
onFragBuffered (event: Events.FRAG_BUFFERED, data: FragBufferedData) {
onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
const { frag, part } = data;
if (frag && frag.type !== 'audio') {
return;
@@ -485,7 +484,9 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
if (this.fragContextChanged(frag)) {
// If a level switch was requested while a fragment was buffering, it will emit the FRAG_BUFFERED event upon completion
// Avoid setting state back to IDLE or concluding the audio switch; otherwise, the switched-to track will not buffer
this.warn(`Fragment ${frag.sn}${part ? ' p: ' + part.index : ''} of level ${frag.level} finished buffering, but was aborted. state: ${this.state}, audioSwitch: ${this.audioSwitch}`);
this.warn(
`Fragment ${frag.sn}${part ? ' p: ' + part.index : ''} of level ${frag.level} finished buffering, but was aborted. state: ${this.state}, audioSwitch: ${this.audioSwitch}`
);
return;
}
this.fragPrevious = frag;
@@ -496,7 +497,7 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
this.fragBufferedComplete(frag, part);
}
onError (data) {
onError(data) {
const frag = data.frag;
// don't handle frag error not related to audio fragment
if (frag && frag.type !== 'audio') {
@@ -504,83 +505,83 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
}
switch (data.details) {
case ErrorDetails.FRAG_LOAD_ERROR:
case ErrorDetails.FRAG_LOAD_TIMEOUT: {
const frag = data.frag;
// don't handle frag error not related to audio fragment
if (frag && frag.type !== 'audio') {
case ErrorDetails.FRAG_LOAD_ERROR:
case ErrorDetails.FRAG_LOAD_TIMEOUT: {
const frag = data.frag;
// don't handle frag error not related to audio fragment
if (frag && frag.type !== 'audio') {
break;
}
if (!data.fatal) {
let loadError = this.fragLoadError;
if (loadError) {
loadError++;
} else {
loadError = 1;
}
const config = this.config;
if (loadError <= config.fragLoadingMaxRetry) {
this.fragLoadError = loadError;
// exponential backoff capped to config.fragLoadingMaxRetryTimeout
const delay = Math.min(Math.pow(2, loadError - 1) * config.fragLoadingRetryDelay, config.fragLoadingMaxRetryTimeout);
this.warn(`Frag loading failed, retry in ${delay} ms`);
this.retryDate = performance.now() + delay;
// retry loading state
this.state = State.FRAG_LOADING_WAITING_RETRY;
} else {
logger.error(`${data.details} reaches max retry, redispatch as fatal ...`);
// switch error to fatal
data.fatal = true;
this.state = State.ERROR;
}
}
break;
}
if (!data.fatal) {
let loadError = this.fragLoadError;
if (loadError) {
loadError++;
} else {
loadError = 1;
case ErrorDetails.AUDIO_TRACK_LOAD_ERROR:
case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT:
case ErrorDetails.KEY_LOAD_ERROR:
case ErrorDetails.KEY_LOAD_TIMEOUT:
// when in ERROR state, don't switch back to IDLE state in case a non-fatal error is received
if (this.state !== State.ERROR && this.state !== State.STOPPED) {
// if fatal error, stop processing, otherwise move to IDLE to retry loading
this.state = data.fatal ? State.ERROR : State.IDLE;
this.warn(`${data.details} while loading frag, switching to ${this.state} state`);
}
const config = this.config;
if (loadError <= config.fragLoadingMaxRetry) {
this.fragLoadError = loadError;
// exponential backoff capped to config.fragLoadingMaxRetryTimeout
const delay = Math.min(Math.pow(2, loadError - 1) * config.fragLoadingRetryDelay, config.fragLoadingMaxRetryTimeout);
this.warn(`Frag loading failed, retry in ${delay} ms`);
this.retryDate = performance.now() + delay;
// retry loading state
this.state = State.FRAG_LOADING_WAITING_RETRY;
} else {
logger.error(`${data.details} reaches max retry, redispatch as fatal ...`);
// switch error to fatal
data.fatal = true;
this.state = State.ERROR;
}
}
break;
}
case ErrorDetails.AUDIO_TRACK_LOAD_ERROR:
case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT:
case ErrorDetails.KEY_LOAD_ERROR:
case ErrorDetails.KEY_LOAD_TIMEOUT:
// when in ERROR state, don't switch back to IDLE state in case a non-fatal error is received
if (this.state !== State.ERROR && this.state !== State.STOPPED) {
// if fatal error, stop processing, otherwise move to IDLE to retry loading
this.state = data.fatal ? State.ERROR : State.IDLE;
this.warn(`${data.details} while loading frag, switching to ${this.state} state`);
}
break;
case ErrorDetails.BUFFER_FULL_ERROR:
// if in appending state
if (data.parent === 'audio' && (this.state === State.PARSING || this.state === State.PARSED)) {
const media = this.mediaBuffer;
const currentTime = this.media.currentTime;
const mediaBuffered = media && BufferHelper.isBuffered(media, currentTime) && BufferHelper.isBuffered(media, currentTime + 0.5);
// reduce max buf len if current position is buffered
if (mediaBuffered) {
const config = this.config;
if (config.maxMaxBufferLength >= config.maxBufferLength) {
// reduce max buffer length as it might be too high. we do this to avoid loop flushing ...
config.maxMaxBufferLength /= 2;
this.warn(`Reduce max buffer length to ${config.maxMaxBufferLength}s`);
break;
case ErrorDetails.BUFFER_FULL_ERROR:
// if in appending state
if (data.parent === 'audio' && (this.state === State.PARSING || this.state === State.PARSED)) {
const media = this.mediaBuffer;
const currentTime = this.media.currentTime;
const mediaBuffered = media && BufferHelper.isBuffered(media, currentTime) && BufferHelper.isBuffered(media, currentTime + 0.5);
// reduce max buf len if current position is buffered
if (mediaBuffered) {
const config = this.config;
if (config.maxMaxBufferLength >= config.maxBufferLength) {
// reduce max buffer length as it might be too high. we do this to avoid loop flushing ...
config.maxMaxBufferLength /= 2;
this.warn(`Reduce max buffer length to ${config.maxMaxBufferLength}s`);
}
this.state = State.IDLE;
} else {
// current position is not buffered, but browser is still complaining about buffer full error
// this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708
// in that case flush the whole audio buffer to recover
this.warn('Buffer full error also media.currentTime is not buffered, flush audio buffer');
this.fragCurrent = null;
// flush everything
this.hls.trigger(Events.BUFFER_FLUSHING, { startOffset: 0, endOffset: Number.POSITIVE_INFINITY, type: 'audio' });
}
this.state = State.IDLE;
} else {
// current position is not buffered, but browser is still complaining about buffer full error
// this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708
// in that case flush the whole audio buffer to recover
this.warn('Buffer full error also media.currentTime is not buffered, flush audio buffer');
this.fragCurrent = null;
// flush everything
this.hls.trigger(Events.BUFFER_FLUSHING, { startOffset: 0, endOffset: Number.POSITIVE_INFINITY, type: 'audio' });
}
}
break;
default:
break;
break;
default:
break;
}
}
onBufferFlushed (event: Events.BUFFER_FLUSHED, { type }: BufferFlushedData) {
onBufferFlushed(event: Events.BUFFER_FLUSHED, { type }: BufferFlushedData) {
/* after successful buffer flushing, filter flushed fragments from bufferedFrags
use mediaBuffered instead of media (so that we will check against video.buffered ranges in case of alt audio track)
*/
@@ -595,7 +596,7 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
this.state = State.IDLE;
}
private _handleTransmuxComplete (transmuxResult: TransmuxerResult) {
private _handleTransmuxComplete(transmuxResult: TransmuxerResult) {
const id = 'audio';
const { hls } = this;
const { remuxResult, chunkMeta } = transmuxResult;
@@ -634,22 +635,28 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
}
if (id3?.samples?.length) {
const emittedID3: FragParsingMetadataData = Object.assign({
frag,
id
}, id3);
const emittedID3: FragParsingMetadataData = Object.assign(
{
frag,
id,
},
id3
);
hls.trigger(Events.FRAG_PARSING_METADATA, emittedID3);
}
if (text) {
const emittedText: FragParsingUserdataData = Object.assign({
frag,
id
}, text);
const emittedText: FragParsingUserdataData = Object.assign(
{
frag,
id,
},
text
);
hls.trigger(Events.FRAG_PARSING_USERDATA, emittedText);
}
}
private _bufferInitSegment (tracks: TrackSet, frag: Fragment, chunkMeta: ChunkMetadata) {
private _bufferInitSegment(tracks: TrackSet, frag: Fragment, chunkMeta: ChunkMetadata) {
if (this.state !== State.PARSING) {
return;
}
@@ -675,7 +682,7 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
data: initSegment,
frag,
part: null,
chunkMeta
chunkMeta,
};
this.hls.trigger(Events.BUFFER_APPENDING, segment);
}
@@ -683,7 +690,7 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
this.tick();
}
protected loadFragment (frag: Fragment, trackDetails: LevelDetails, targetBufferTime: number) {
protected loadFragment(frag: Fragment, trackDetails: LevelDetails, targetBufferTime: number) {
// only load if fragment is not loaded or if in audio switch
const fragState = this.fragmentTracker.getState(frag);
this.fragCurrent = frag;
@@ -703,14 +710,14 @@ class AudioStreamController extends BaseStreamController implements NetworkCompo
}
}
private completeAudioSwitch () {
private completeAudioSwitch() {
const { hls, media, trackId } = this;
if (media) {
this.log('Switching audio track : flushing all audio');
hls.trigger(Events.BUFFER_FLUSHING, {
startOffset: 0,
endOffset: Number.POSITIVE_INFINITY,
type: 'audio'
type: 'audio',
});
}
this.audioSwitch = false;
+22 -31
View File
@@ -1,12 +1,6 @@
import { Events } from '../events';
import { ErrorTypes, ErrorDetails } from '../errors';
import {
ManifestParsedData,
AudioTracksUpdatedData,
ErrorData,
LevelLoadingData,
AudioTrackLoadedData
} from '../types/events';
import { ManifestParsedData, AudioTracksUpdatedData, ErrorData, LevelLoadingData, AudioTrackLoadedData } from '../types/events';
import BasePlaylistController from './base-playlist-controller';
import { PlaylistContextType } from '../types/loader';
import type Hls from '../hls';
@@ -20,12 +14,12 @@ class AudioTrackController extends BasePlaylistController {
private trackId: number = -1;
private selectDefaultTrack: boolean = true;
constructor (hls: Hls) {
constructor(hls: Hls) {
super(hls, '[audio-track-controller]');
this.registerListeners();
}
private registerListeners () {
private registerListeners() {
const { hls } = this;
hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this);
@@ -34,7 +28,7 @@ class AudioTrackController extends BasePlaylistController {
hls.on(Events.ERROR, this.onError, this);
}
private unregisterListeners () {
private unregisterListeners() {
const { hls } = this;
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this);
@@ -43,12 +37,12 @@ class AudioTrackController extends BasePlaylistController {
hls.off(Events.ERROR, this.onError, this);
}
public destroy () {
public destroy() {
this.unregisterListeners();
super.destroy();
}
protected onManifestLoading (): void {
protected onManifestLoading(): void {
this.tracks = [];
this.groupId = null;
this.tracksInGroup = [];
@@ -56,11 +50,11 @@ class AudioTrackController extends BasePlaylistController {
this.selectDefaultTrack = true;
}
protected onManifestParsed (event: Events.MANIFEST_PARSED, data: ManifestParsedData): void {
protected onManifestParsed(event: Events.MANIFEST_PARSED, data: ManifestParsedData): void {
this.tracks = data.audioTracks || [];
}
protected onAudioTrackLoaded (event: Events.AUDIO_TRACK_LOADED, data: AudioTrackLoadedData): void {
protected onAudioTrackLoaded(event: Events.AUDIO_TRACK_LOADED, data: AudioTrackLoadedData): void {
const { id, details } = data;
const currentTrack = this.tracksInGroup[id];
@@ -86,7 +80,7 @@ class AudioTrackController extends BasePlaylistController {
* If group-ID got update, we re-select the appropriate audio-track with this group-ID matching the currently
* selected one (based on NAME property).
*/
protected onLevelLoading (event: Events.LEVEL_LOADING, data: LevelLoadingData): void {
protected onLevelLoading(event: Events.LEVEL_LOADING, data: LevelLoadingData): void {
const levelInfo = this.hls.levels[data.level];
if (!levelInfo?.audioGroupIds) {
@@ -97,11 +91,10 @@ class AudioTrackController extends BasePlaylistController {
if (this.groupId !== audioGroupId) {
this.groupId = audioGroupId;
const audioTracks = this.tracks.filter((track): boolean =>
!audioGroupId || track.groupId === audioGroupId);
const audioTracks = this.tracks.filter((track): boolean => !audioGroupId || track.groupId === audioGroupId);
// Disable selectDefaultTrack if there are no default tracks
if (this.selectDefaultTrack && !audioTracks.some(track => track.default)) {
if (this.selectDefaultTrack && !audioTracks.some((track) => track.default)) {
this.selectDefaultTrack = false;
}
@@ -113,34 +106,32 @@ class AudioTrackController extends BasePlaylistController {
}
}
protected onError (event: Events.ERROR, data: ErrorData): void {
protected onError(event: Events.ERROR, data: ErrorData): void {
super.onError(event, data);
if (data.fatal || !data.context) {
return;
}
if (data.context.type === PlaylistContextType.AUDIO_TRACK &&
data.context.id === this.trackId &&
data.context.groupId === this.groupId) {
if (data.context.type === PlaylistContextType.AUDIO_TRACK && data.context.id === this.trackId && data.context.groupId === this.groupId) {
this.retryLoadingOrFail(data);
}
}
get audioTracks (): MediaPlaylist[] {
get audioTracks(): MediaPlaylist[] {
return this.tracksInGroup;
}
get audioTrack (): number {
get audioTrack(): number {
return this.trackId;
}
set audioTrack (newId: number) {
set audioTrack(newId: number) {
// If audio track is selected from API then don't choose from the manifest default track
this.selectDefaultTrack = false;
this.setAudioTrack(newId);
}
private setAudioTrack (newId: number): void {
private setAudioTrack(newId: number): void {
const tracks = this.tracksInGroup;
// noop on same audio track id as already set
if (this.trackId === newId && tracks[newId]?.details) {
@@ -166,7 +157,7 @@ class AudioTrackController extends BasePlaylistController {
this.loadPlaylist(hlsUrlParameters);
}
private selectInitialTrack (): void {
private selectInitialTrack(): void {
const audioTracks = this.tracksInGroup;
console.assert(audioTracks.length, 'Initial audio track should be selected when tracks are known');
const currentAudioTrackName = audioTracks[this.trackId]?.name;
@@ -180,12 +171,12 @@ class AudioTrackController extends BasePlaylistController {
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.AUDIO_TRACK_LOAD_ERROR,
fatal: true
fatal: true,
});
}
}
private findTrackId (name?: string): number {
private findTrackId(name?: string): number {
const audioTracks = this.tracksInGroup;
for (let i = 0; i < audioTracks.length; i++) {
const track = audioTracks[i];
@@ -198,7 +189,7 @@ class AudioTrackController extends BasePlaylistController {
return -1;
}
protected loadPlaylist (hlsUrlParameters?: HlsUrlParameters): void {
protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void {
const audioTrack = this.tracksInGroup[this.trackId];
if (this.shouldLoadTrack(audioTrack)) {
const id = audioTrack.id;
@@ -218,7 +209,7 @@ class AudioTrackController extends BasePlaylistController {
url,
id,
groupId,
deliveryDirectives: hlsUrlParameters || null
deliveryDirectives: hlsUrlParameters || null,
});
}
}
+12 -12
View File
@@ -19,39 +19,39 @@ export default class BasePlaylistController implements NetworkComponentAPI {
protected readonly log: (msg: any) => void;
protected readonly warn: (msg: any) => void;
constructor (hls: Hls, logPrefix: string) {
constructor(hls: Hls, logPrefix: string) {
this.log = logger.log.bind(logger, `${logPrefix}:`);
this.warn = logger.warn.bind(logger, `${logPrefix}:`);
this.hls = hls;
}
public destroy (): void {
public destroy(): void {
this.clearTimer();
}
protected onError (event: Events.ERROR, data: ErrorData): void {
protected onError(event: Events.ERROR, data: ErrorData): void {
if (data.fatal && data.type === ErrorTypes.NETWORK_ERROR) {
this.clearTimer();
}
}
protected clearTimer (): void {
protected clearTimer(): void {
clearTimeout(this.timer);
this.timer = -1;
}
public startLoad (): void {
public startLoad(): void {
this.canLoad = true;
this.retryCount = 0;
this.loadPlaylist();
}
public stopLoad (): void {
public stopLoad(): void {
this.canLoad = false;
this.clearTimer();
}
protected switchParams (playlistUri: string, previous?: LevelDetails): HlsUrlParameters | undefined {
protected switchParams(playlistUri: string, previous?: LevelDetails): HlsUrlParameters | undefined {
const renditionReports = previous?.renditionReports;
if (renditionReports) {
for (let i = 0; i < renditionReports.length; i++) {
@@ -74,13 +74,13 @@ export default class BasePlaylistController implements NetworkComponentAPI {
}
}
protected loadPlaylist (hlsUrlParameters?: HlsUrlParameters): void {}
protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void {}
protected shouldLoadTrack (track: MediaPlaylist): boolean {
protected shouldLoadTrack(track: MediaPlaylist): boolean {
return this.canLoad && track && !!track.url && (!track.details || track.details.live);
}
protected playlistLoaded (index: number, data: LevelLoadedData | AudioTrackLoadedData | TrackLoadedData, previousDetails?: LevelDetails) {
protected playlistLoaded(index: number, data: LevelLoadedData | AudioTrackLoadedData | TrackLoadedData, previousDetails?: LevelDetails) {
const { details, stats } = data;
// Set last updated date-time
@@ -91,7 +91,7 @@ export default class BasePlaylistController implements NetworkComponentAPI {
if (details.live || previousDetails?.live) {
details.reloaded(previousDetails);
if (previousDetails) {
this.log(`live playlist ${index} ${details.advanced ? ('REFRESHED ' + details.lastPartSn + '-' + details.lastPartIndex) : 'MISSED'}`);
this.log(`live playlist ${index} ${details.advanced ? 'REFRESHED ' + details.lastPartSn + '-' + details.lastPartIndex : 'MISSED'}`);
}
// Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments
if (previousDetails && details.fragments.length > 0) {
@@ -159,7 +159,7 @@ export default class BasePlaylistController implements NetworkComponentAPI {
}
}
protected retryLoadingOrFail (errorEvent: ErrorData): boolean {
protected retryLoadingOrFail(errorEvent: ErrorData): boolean {
const { config } = this.hls;
const retry = this.retryCount < config.levelLoadingMaxRetry;
if (retry) {
+62 -57
View File
@@ -6,13 +6,18 @@ import { getMediaSource } from '../utils/mediasource-helper';
import { ElementaryStreamTypes } from '../loader/fragment';
import type { TrackSet } from '../types/track';
import BufferOperationQueue from './buffer-operation-queue';
import {
BufferOperation,
SourceBuffers,
SourceBufferName,
SourceBufferListeners
} from '../types/buffer';
import type { LevelUpdatedData, BufferAppendingData, MediaAttachingData, ManifestParsedData, BufferCodecsData, LevelPTSUpdatedData, BufferEOSData, BufferFlushingData, FragParsedData } from '../types/events';
import { BufferOperation, SourceBuffers, SourceBufferName, SourceBufferListeners } from '../types/buffer';
import type {
LevelUpdatedData,
BufferAppendingData,
MediaAttachingData,
ManifestParsedData,
BufferCodecsData,
LevelPTSUpdatedData,
BufferEOSData,
BufferFlushingData,
FragParsedData,
} from '../types/events';
import type { ComponentAPI } from '../types/component-api';
import type Hls from '../hls';
@@ -54,17 +59,17 @@ export default class BufferController implements ComponentAPI {
public pendingTracks: TrackSet = {};
public sourceBuffer!: SourceBuffers;
constructor (hls: Hls) {
constructor(hls: Hls) {
this.hls = hls;
this._initSourceBuffer();
this.registerListeners();
}
public destroy () {
public destroy() {
this.unregisterListeners();
}
protected registerListeners () {
protected registerListeners() {
const { hls } = this;
hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -79,7 +84,7 @@ export default class BufferController implements ComponentAPI {
hls.on(Events.FRAG_PARSED, this.onFragParsed, this);
}
protected unregisterListeners () {
protected unregisterListeners() {
const { hls } = this;
hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -94,17 +99,17 @@ export default class BufferController implements ComponentAPI {
hls.off(Events.FRAG_PARSED, this.onFragParsed, this);
}
private _initSourceBuffer () {
private _initSourceBuffer() {
this.sourceBuffer = {};
this.operationQueue = new BufferOperationQueue(this.sourceBuffer);
this.listeners = {
audio: [],
video: [],
audiovideo: []
audiovideo: [],
};
}
protected onManifestParsed (event: Events.MANIFEST_PARSED, data: ManifestParsedData) {
protected onManifestParsed(event: Events.MANIFEST_PARSED, data: ManifestParsedData) {
// in case of alt audio 2 BUFFER_CODECS events will be triggered, one per stream controller
// sourcebuffers will be created all at once when the expected nb of tracks will be reached
// in case alt audio is not used, only one BUFFER_CODEC event will be fired from main stream controller
@@ -118,10 +123,10 @@ export default class BufferController implements ComponentAPI {
logger.log(`${this.bufferCodecEventsExpected} bufferCodec event(s) expected`);
}
protected onMediaAttaching (event: Events.MEDIA_ATTACHING, data: MediaAttachingData) {
const media = this.media = data.media;
protected onMediaAttaching(event: Events.MEDIA_ATTACHING, data: MediaAttachingData) {
const media = (this.media = data.media);
if (media && MediaSource) {
const ms = this.mediaSource = new MediaSource();
const ms = (this.mediaSource = new MediaSource());
// MediaSource listeners are arrow functions with a lexical scope, and do not need to be bound
ms.addEventListener('sourceopen', this._onMediaSourceOpen);
ms.addEventListener('sourceended', this._onMediaSourceEnded);
@@ -133,7 +138,7 @@ export default class BufferController implements ComponentAPI {
}
}
protected onMediaDetaching () {
protected onMediaDetaching() {
logger.log('[buffer-controller]: media source detaching');
const { media, mediaSource, _objectUrl } = this;
if (mediaSource) {
@@ -182,9 +187,9 @@ export default class BufferController implements ComponentAPI {
this.hls.trigger(Events.MEDIA_DETACHED, undefined);
}
protected onBufferReset () {
protected onBufferReset() {
const sourceBuffer = this.sourceBuffer;
this.getSourceBufferTypes().forEach(type => {
this.getSourceBufferTypes().forEach((type) => {
const sb = sourceBuffer[type];
try {
if (sb) {
@@ -203,14 +208,14 @@ export default class BufferController implements ComponentAPI {
this._initSourceBuffer();
}
protected onBufferCodecs (event: Events.BUFFER_CODECS, data: BufferCodecsData) {
protected onBufferCodecs(event: Events.BUFFER_CODECS, data: BufferCodecsData) {
// if source buffer(s) not created yet, appended buffer tracks in this.pendingTracks
// if sourcebuffers already created, do nothing ...
if (Object.keys(this.sourceBuffer).length) {
return;
}
Object.keys(data).forEach(trackName => {
Object.keys(data).forEach((trackName) => {
this.pendingTracks[trackName] = data[trackName];
});
@@ -220,7 +225,7 @@ export default class BufferController implements ComponentAPI {
}
}
protected onBufferAppending (event: Events.BUFFER_APPENDING, eventData: BufferAppendingData) {
protected onBufferAppending(event: Events.BUFFER_APPENDING, eventData: BufferAppendingData) {
const { hls, operationQueue } = this;
const { data, type, frag, part, chunkMeta } = eventData;
const chunkStats = chunkMeta.buffering[type];
@@ -271,7 +276,7 @@ export default class BufferController implements ComponentAPI {
parent: frag.type,
details: ErrorDetails.BUFFER_APPEND_ERROR,
err,
fatal: false
fatal: false,
};
if (err.code === DOMException.QUOTA_EXCEEDED_ERR) {
@@ -290,12 +295,12 @@ export default class BufferController implements ComponentAPI {
}
}
hls.trigger(Events.ERROR, event);
}
},
};
operationQueue.append(operation, type);
}
protected onBufferFlushing (event: Events.BUFFER_FLUSHING, data: BufferFlushingData) {
protected onBufferFlushing(event: Events.BUFFER_FLUSHING, data: BufferFlushingData) {
const { operationQueue } = this;
const flushOperation = (type): BufferOperation => ({
execute: this.removeExecutor.bind(this, type, data.startOffset, data.endOffset),
@@ -308,7 +313,7 @@ export default class BufferController implements ComponentAPI {
},
onError: (e) => {
logger.warn(`[buffer-controller]: Failed to remove from ${type} SourceBuffer`, e);
}
},
});
if (data.type) {
@@ -319,7 +324,7 @@ export default class BufferController implements ComponentAPI {
}
}
protected onFragParsed (event: Events.FRAG_PARSED, data: FragParsedData) {
protected onFragParsed(event: Events.FRAG_PARSED, data: FragParsedData) {
const { frag, part } = data;
const buffersAppendedTo: Array<SourceBufferName> = [];
const elementaryStreams = part ? part.elementaryStreams : frag.elementaryStreams;
@@ -345,7 +350,7 @@ export default class BufferController implements ComponentAPI {
frag,
part,
stats,
id: frag.type
id: frag.type,
});
};
@@ -361,7 +366,7 @@ export default class BufferController implements ComponentAPI {
// on BUFFER_EOS mark matching sourcebuffer(s) as ended and trigger checkEos()
// an undefined data.type will mark all buffers as EOS.
protected onBufferEos (event: Events.BUFFER_EOS, data: BufferEOSData) {
protected onBufferEos(event: Events.BUFFER_EOS, data: BufferEOSData) {
for (const type in this.sourceBuffer) {
if (!data.type || data.type === type) {
const sb = this.sourceBuffer[type as SourceBufferName];
@@ -383,7 +388,7 @@ export default class BufferController implements ComponentAPI {
this.blockBuffers(endStream);
}
protected onLevelUpdated (event: Events.LEVEL_UPDATED, { details }: LevelUpdatedData) {
protected onLevelUpdated(event: Events.LEVEL_UPDATED, { details }: LevelUpdatedData) {
if (!details.fragments.length) {
return;
}
@@ -407,7 +412,7 @@ export default class BufferController implements ComponentAPI {
// `SourceBuffer.abort()` and adjusting `SourceBuffer.timestampOffset` if `SourceBuffer.updating` is false or awaiting `updateend`
// event if SB is in updating state.
// More info here: https://github.com/video-dev/hls.js/issues/332#issuecomment-257986486
protected onLevelPtsUpdated (event: Events.LEVEL_PTS_UPDATED, data: LevelPTSUpdatedData) {
protected onLevelPtsUpdated(event: Events.LEVEL_PTS_UPDATED, data: LevelPTSUpdatedData) {
const { operationQueue, sourceBuffer, tracks } = this;
const type = data.type;
const audioTrack = tracks.audio;
@@ -429,20 +434,20 @@ export default class BufferController implements ComponentAPI {
// updating is false
if (audioBuffer.updating) {
const operation = {
execute () {
execute() {
logger.log(`[buffer-controller]: Aborting the ${type} SourceBuffer`);
audioBuffer.abort();
},
onStart () {
onStart() {
logger.debug(`[buffer-controller]: Starting abort on source buffer ${type}`);
},
onComplete () {
onComplete() {
logger.log(`[buffer-controller]: Updating audio SourceBuffer timestampOffset to ${start}`);
audioBuffer.timestampOffset = start;
},
onError (error) {
onError(error) {
logger.warn('[buffer-controller]: Failed to abort the audio SourceBuffer', error);
}
},
};
operationQueue.insertAbort(operation, type);
} else {
@@ -455,7 +460,7 @@ export default class BufferController implements ComponentAPI {
}
}
flushLiveBackBuffer () {
flushLiveBackBuffer() {
// clear back buffer for live only
const { hls, _levelTargetDuration, _live, media, sourceBuffer } = this;
if (!media || !_live || _levelTargetDuration === null) {
@@ -487,7 +492,7 @@ export default class BufferController implements ComponentAPI {
* 'liveDurationInfinity` is set to `true`
* More details: https://github.com/video-dev/hls.js/issues/355
*/
private updateMediaElementDuration (levelDuration: number) {
private updateMediaElementDuration(levelDuration: number) {
if (!this.media || !this.mediaSource || this.mediaSource.readyState !== 'open') {
this.duration = levelDuration;
return;
@@ -512,7 +517,7 @@ export default class BufferController implements ComponentAPI {
}
}
updateSeekableRange (levelDetails) {
updateSeekableRange(levelDetails) {
const mediaSource = this.mediaSource;
const fragments = levelDetails.fragments;
const len = fragments.length;
@@ -523,7 +528,7 @@ export default class BufferController implements ComponentAPI {
}
}
protected checkPendingTracks () {
protected checkPendingTracks() {
const { bufferCodecEventsExpected, operationQueue, pendingTracks } = this;
// Check if we've received all of the expected bufferCodec events. When none remain, create all the sourceBuffers at once.
@@ -542,7 +547,7 @@ export default class BufferController implements ComponentAPI {
}
}
protected createSourceBuffers (tracks: TrackSet) {
protected createSourceBuffers(tracks: TrackSet) {
const { sourceBuffer, mediaSource } = this;
if (!mediaSource) {
throw Error('createSourceBuffers called when mediaSource was null');
@@ -559,7 +564,7 @@ export default class BufferController implements ComponentAPI {
const mimeType = `${track.container};codecs=${codec}`;
logger.log(`[buffer-controller]: creating sourceBuffer(${mimeType})`);
try {
const sb = sourceBuffer[trackName] = mediaSource.addSourceBuffer(mimeType);
const sb = (sourceBuffer[trackName] = mediaSource.addSourceBuffer(mimeType));
const sbName = trackName as SourceBufferName;
this.addBufferListener(sbName, 'updatestart', this._onSBUpdateStart);
this.addBufferListener(sbName, 'updateend', this._onSBUpdateEnd);
@@ -569,7 +574,7 @@ export default class BufferController implements ComponentAPI {
codec: codec,
container: track.container,
levelCodec: track.levelCodec,
id: track.id
id: track.id,
};
} catch (err) {
logger.error(`[buffer-controller]: error while trying to add sourceBuffer: ${err.message}`);
@@ -578,7 +583,7 @@ export default class BufferController implements ComponentAPI {
details: ErrorDetails.BUFFER_ADD_CODEC_ERROR,
fatal: false,
error: err,
mimeType: mimeType
mimeType: mimeType,
});
}
}
@@ -612,20 +617,20 @@ export default class BufferController implements ComponentAPI {
logger.log('[buffer-controller]: Media source ended');
};
private _onSBUpdateStart (type: SourceBufferName) {
private _onSBUpdateStart(type: SourceBufferName) {
const { operationQueue } = this;
const operation = operationQueue.current(type);
operation.onStart();
}
private _onSBUpdateEnd (type: SourceBufferName) {
private _onSBUpdateEnd(type: SourceBufferName) {
const { operationQueue } = this;
const operation = operationQueue.current(type);
operation.onComplete();
operationQueue.shiftAndExecuteNext(type);
}
private _onSBUpdateError (type: SourceBufferName, event: Event) {
private _onSBUpdateError(type: SourceBufferName, event: Event) {
logger.error(`[buffer-controller]: ${type} SourceBuffer error`, event);
// according to http://www.w3.org/TR/media-source/#sourcebuffer-append-error
// SourceBuffer errors are not necessarily fatal; if so, the HTMLMediaElement will fire an error event
@@ -638,7 +643,7 @@ export default class BufferController implements ComponentAPI {
}
// This method must result in an updateend event; if remove is not called, _onSBUpdateEnd must be called manually
private removeExecutor (type: SourceBufferName, startOffset: number, endOffset: number) {
private removeExecutor(type: SourceBufferName, startOffset: number, endOffset: number) {
const { media, operationQueue, sourceBuffer } = this;
const sb = sourceBuffer[type];
if (!media || !sb) {
@@ -660,7 +665,7 @@ export default class BufferController implements ComponentAPI {
}
// This method must result in an updateend event; if append is not called, _onSBUpdateEnd must be called manually
private appendExecutor (data: Uint8Array, type: SourceBufferName) {
private appendExecutor(data: Uint8Array, type: SourceBufferName) {
const { operationQueue, sourceBuffer } = this;
const sb = sourceBuffer[type];
if (!sb) {
@@ -677,7 +682,7 @@ export default class BufferController implements ComponentAPI {
// Enqueues an operation to each SourceBuffer queue which, upon execution, resolves a promise. When all promises
// resolve, the onUnblocked function is executed. Functions calling this method do not need to unblock the queue
// upon completion, since we already do it here
private blockBuffers (onUnblocked: Function, buffers: Array<SourceBufferName> = this.getSourceBufferTypes()) {
private blockBuffers(onUnblocked: Function, buffers: Array<SourceBufferName> = this.getSourceBufferTypes()) {
if (!buffers.length) {
logger.log('[buffer-controller]: Blocking operation requested, but no SourceBuffers exist');
Promise.resolve(onUnblocked);
@@ -686,11 +691,11 @@ export default class BufferController implements ComponentAPI {
const { operationQueue } = this;
// logger.debug(`[buffer-controller]: Blocking ${buffers} SourceBuffer`);
const blockingOperations = buffers.map(type => operationQueue.appendBlocker(type as SourceBufferName));
const blockingOperations = buffers.map((type) => operationQueue.appendBlocker(type as SourceBufferName));
Promise.all(blockingOperations).then(() => {
// logger.debug(`[buffer-controller]: Blocking operation resolved; unblocking ${buffers} SourceBuffer`);
onUnblocked();
buffers.forEach(type => {
buffers.forEach((type) => {
const sb = this.sourceBuffer[type];
// Only cycle the queue if the SB is not updating. There's a bug in Chrome which sets the SB updating flag to
// true when changing the MediaSource duration (https://bugs.chromium.org/p/chromium/issues/detail?id=959359&can=2&q=mediasource%20duration)
@@ -702,11 +707,11 @@ export default class BufferController implements ComponentAPI {
});
}
private getSourceBufferTypes () : Array<SourceBufferName> {
private getSourceBufferTypes(): Array<SourceBufferName> {
return Object.keys(this.sourceBuffer) as Array<SourceBufferName>;
}
private addBufferListener (type: SourceBufferName, event: string, fn: Function) {
private addBufferListener(type: SourceBufferName, event: string, fn: Function) {
const buffer = this.sourceBuffer[type];
if (!buffer) {
return;
@@ -716,12 +721,12 @@ export default class BufferController implements ComponentAPI {
buffer.addEventListener(event, listener);
}
private removeBufferListeners (type: SourceBufferName) {
private removeBufferListeners(type: SourceBufferName) {
const buffer = this.sourceBuffer[type];
if (!buffer) {
return;
}
this.listeners[type].forEach(l => {
this.listeners[type].forEach((l) => {
buffer.removeEventListener(l.event, l.listener);
});
}
+9 -9
View File
@@ -6,14 +6,14 @@ export default class BufferOperationQueue {
private queues: BufferOperationQueues = {
video: [],
audio: [],
audiovideo: []
audiovideo: [],
};
constructor (sourceBufferReference: SourceBuffers) {
constructor(sourceBufferReference: SourceBuffers) {
this.buffers = sourceBufferReference;
}
public append (operation: BufferOperation, type: SourceBufferName) {
public append(operation: BufferOperation, type: SourceBufferName) {
const queue = this.queues[type];
queue.push(operation);
if (queue.length === 1 && this.buffers[type]) {
@@ -21,13 +21,13 @@ export default class BufferOperationQueue {
}
}
public insertAbort (operation: BufferOperation, type: SourceBufferName) {
public insertAbort(operation: BufferOperation, type: SourceBufferName) {
const queue = this.queues[type];
queue.unshift(operation);
this.executeNext(type, true);
}
public appendBlocker (type: SourceBufferName) : Promise<{}> {
public appendBlocker(type: SourceBufferName): Promise<{}> {
let execute;
const promise: Promise<{}> = new Promise((resolve) => {
execute = resolve;
@@ -36,14 +36,14 @@ export default class BufferOperationQueue {
execute,
onStart: () => {},
onComplete: () => {},
onError: () => {}
onError: () => {},
};
this.append(operation, type);
return promise;
}
public executeNext (type: SourceBufferName, ignoreUpdating?: boolean) {
public executeNext(type: SourceBufferName, ignoreUpdating?: boolean) {
const { buffers, queues } = this;
const sb = buffers[type];
console.assert(!sb || ignoreUpdating || !sb.updating, `${type} sourceBuffer must exist, and must not be updating`);
@@ -67,12 +67,12 @@ export default class BufferOperationQueue {
}
}
public shiftAndExecuteNext (type: SourceBufferName) {
public shiftAndExecuteNext(type: SourceBufferName) {
this.queues[type].shift();
this.executeNext(type);
}
public current (type: SourceBufferName) {
public current(type: SourceBufferName) {
return this.queues[type][0];
}
}
+30 -30
View File
@@ -1,6 +1,6 @@
/*
* cap stream level to media size dimension controller
*/
*/
import { Events } from '../events';
import type { Level } from '../types/level';
@@ -19,9 +19,9 @@ class CapLevelController implements ComponentAPI {
private hls: Hls;
private streamController?: StreamController;
public clientRect: { width: number, height: number } | null;
public clientRect: { width: number; height: number } | null;
constructor (hls: Hls) {
constructor(hls: Hls) {
this.hls = hls;
this.autoLevelCapping = Number.POSITIVE_INFINITY;
this.levels = [];
@@ -34,11 +34,11 @@ class CapLevelController implements ComponentAPI {
this.registerListeners();
}
public setStreamController (streamController: StreamController) {
public setStreamController(streamController: StreamController) {
this.streamController = streamController;
}
public destroy () {
public destroy() {
this.unregisterListener();
if (this.hls.config.capLevelToPlayerSize) {
this.media = null;
@@ -47,7 +47,7 @@ class CapLevelController implements ComponentAPI {
}
}
protected registerListeners () {
protected registerListeners() {
const { hls } = this;
hls.on(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this);
hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
@@ -57,7 +57,7 @@ class CapLevelController implements ComponentAPI {
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
}
protected unregisterListener () {
protected unregisterListener() {
const { hls } = this;
hls.off(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this);
hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
@@ -67,18 +67,18 @@ class CapLevelController implements ComponentAPI {
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
}
protected onFpsDropLevelCapping (event: Events.FPS_DROP_LEVEL_CAPPING, data: FPSDropLevelCappingData) {
protected onFpsDropLevelCapping(event: Events.FPS_DROP_LEVEL_CAPPING, data: FPSDropLevelCappingData) {
// Don't add a restricted level more than once
if (CapLevelController.isLevelAllowed(data.droppedLevel, this.restrictedLevels)) {
this.restrictedLevels.push(data.droppedLevel);
}
}
protected onMediaAttaching (event: Events.MEDIA_ATTACHING, data: MediaAttachingData) {
protected onMediaAttaching(event: Events.MEDIA_ATTACHING, data: MediaAttachingData) {
this.media = data.media instanceof HTMLVideoElement ? data.media : null;
}
protected onManifestParsed (event: Events.MANIFEST_PARSED, data: ManifestParsedData) {
protected onManifestParsed(event: Events.MANIFEST_PARSED, data: ManifestParsedData) {
const hls = this.hls;
this.restrictedLevels = [];
this.levels = data.levels;
@@ -91,7 +91,7 @@ class CapLevelController implements ComponentAPI {
// Only activate capping when playing a video stream; otherwise, multi-bitrate audio-only streams will be restricted
// to the first level
protected onBufferCodecs (event: Events.BUFFER_CODECS, data: BufferCodecsData) {
protected onBufferCodecs(event: Events.BUFFER_CODECS, data: BufferCodecsData) {
const hls = this.hls;
if (hls.config.capLevelToPlayerSize && data.video) {
// If the manifest did not signal a video codec capping has been deferred until we're certain video is present
@@ -99,15 +99,15 @@ class CapLevelController implements ComponentAPI {
}
}
protected onLevelsUpdated (event: Events.LEVELS_UPDATED, data: LevelsUpdatedData) {
protected onLevelsUpdated(event: Events.LEVELS_UPDATED, data: LevelsUpdatedData) {
this.levels = data.levels;
}
protected onMediaDetaching () {
protected onMediaDetaching() {
this.stopCapping();
}
detectPlayerSize () {
detectPlayerSize() {
if (this.media && this.mediaHeight > 0 && this.mediaWidth > 0) {
const levelsLength = this.levels ? this.levels.length : 0;
if (levelsLength) {
@@ -124,22 +124,20 @@ class CapLevelController implements ComponentAPI {
}
/*
* returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled)
*/
getMaxLevel (capLevelIndex: number): number {
* returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled)
*/
getMaxLevel(capLevelIndex: number): number {
if (!this.levels) {
return -1;
}
const validLevels = this.levels.filter((level, index) =>
CapLevelController.isLevelAllowed(index, this.restrictedLevels) && index <= capLevelIndex
);
const validLevels = this.levels.filter((level, index) => CapLevelController.isLevelAllowed(index, this.restrictedLevels) && index <= capLevelIndex);
this.clientRect = null;
return CapLevelController.getMaxLevelByMediaSize(validLevels, this.mediaWidth, this.mediaHeight);
}
startCapping () {
startCapping() {
if (this.timer) {
// Don't reset capping if started twice; this can happen if the manifest signals a video codec
return;
@@ -151,7 +149,7 @@ class CapLevelController implements ComponentAPI {
this.detectPlayerSize();
}
stopCapping () {
stopCapping() {
this.restrictedLevels = [];
this.firstLevel = -1;
this.autoLevelCapping = Number.POSITIVE_INFINITY;
@@ -161,14 +159,14 @@ class CapLevelController implements ComponentAPI {
}
}
getDimensions (): { width: number, height: number } {
getDimensions(): { width: number; height: number } {
if (this.clientRect) {
return this.clientRect;
}
const media = this.media;
const boundsRect = {
width: 0,
height: 0
height: 0,
};
if (media) {
@@ -186,27 +184,29 @@ class CapLevelController implements ComponentAPI {
return boundsRect;
}
get mediaWidth (): number {
get mediaWidth(): number {
return this.getDimensions().width * CapLevelController.contentScaleFactor;
}
get mediaHeight (): number {
get mediaHeight(): number {
return this.getDimensions().height * CapLevelController.contentScaleFactor;
}
static get contentScaleFactor (): number {
static get contentScaleFactor(): number {
let pixelRatio = 1;
try {
pixelRatio = self.devicePixelRatio;
} catch (e) { /* no-op */ }
} catch (e) {
/* no-op */
}
return pixelRatio;
}
static isLevelAllowed (level: number, restrictedLevels: Array<number> = []): boolean {
static isLevelAllowed(level: number, restrictedLevels: Array<number> = []): boolean {
return restrictedLevels.indexOf(level) === -1;
}
static getMaxLevelByMediaSize (levels: Array<Level>, width: number, height: number): number {
static getMaxLevelByMediaSize(levels: Array<Level>, width: number, height: number): number {
if (!levels || (levels && !levels.length)) {
return -1;
}
+126 -127
View File
@@ -24,11 +24,8 @@ const MAX_LICENSE_REQUEST_FAILURES = 3;
* @returns {Array<MediaSystemConfiguration>} An array of supported configurations
*/
const createWidevineMediaKeySystemConfigurations = function (
audioCodecs: string[],
videoCodecs: string[],
drmSystemOptions: DRMSystemOptions
): MediaKeySystemConfiguration[] { /* jshint ignore:line */
const createWidevineMediaKeySystemConfigurations = function (audioCodecs: string[], videoCodecs: string[], drmSystemOptions: DRMSystemOptions): MediaKeySystemConfiguration[] {
/* jshint ignore:line */
const baseConfig: MediaKeySystemConfiguration = {
// initDataTypes: ['keyids', 'mp4'],
// label: "",
@@ -36,25 +33,23 @@ const createWidevineMediaKeySystemConfigurations = function (
// distinctiveIdentifier: "not-allowed", // or "required" ?
// sessionTypes: ['temporary'],
audioCapabilities: [], // { contentType: 'audio/mp4; codecs="mp4a.40.2"' }
videoCapabilities: [] // { contentType: 'video/mp4; codecs="avc1.42E01E"' }
videoCapabilities: [], // { contentType: 'video/mp4; codecs="avc1.42E01E"' }
};
audioCodecs.forEach((codec) => {
baseConfig.audioCapabilities!.push({
contentType: `audio/mp4; codecs="${codec}"`,
robustness: drmSystemOptions.audioRobustness || ''
robustness: drmSystemOptions.audioRobustness || '',
});
});
videoCodecs.forEach((codec) => {
baseConfig.videoCapabilities!.push({
contentType: `video/mp4; codecs="${codec}"`,
robustness: drmSystemOptions.videoRobustness || ''
robustness: drmSystemOptions.videoRobustness || '',
});
});
return [
baseConfig
];
return [baseConfig];
};
/**
@@ -76,16 +71,16 @@ const getSupportedMediaKeySystemConfigurations = function (
drmSystemOptions: DRMSystemOptions
): MediaKeySystemConfiguration[] {
switch (keySystem) {
case KeySystems.WIDEVINE:
return createWidevineMediaKeySystemConfigurations(audioCodecs, videoCodecs, drmSystemOptions);
default:
throw new Error(`Unknown key-system: ${keySystem}`);
case KeySystems.WIDEVINE:
return createWidevineMediaKeySystemConfigurations(audioCodecs, videoCodecs, drmSystemOptions);
default:
throw new Error(`Unknown key-system: ${keySystem}`);
}
};
interface MediaKeysListItem {
mediaKeys?: MediaKeys,
mediaKeysSession?: MediaKeySession,
mediaKeys?: MediaKeys;
mediaKeysSession?: MediaKeySession;
mediaKeysSessionInitialized: boolean;
mediaKeySystemAccess: MediaKeySystemAccess;
mediaKeySystemDomain: KeySystems;
@@ -115,10 +110,10 @@ class EMEController implements ComponentAPI {
private mediaKeysPromise: Promise<MediaKeys> | null = null;
/**
* @constructs
* @param {Hls} hls Our Hls.js instance
*/
constructor (hls: Hls) {
* @constructs
* @param {Hls} hls Our Hls.js instance
*/
constructor(hls: Hls) {
this.hls = hls;
this._config = hls.config;
@@ -131,17 +126,17 @@ class EMEController implements ComponentAPI {
this._registerListeners();
}
public destroy () {
public destroy() {
this._unregisterListeners();
}
private _registerListeners () {
private _registerListeners() {
this.hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
this.hls.on(Events.MEDIA_DETACHED, this.onMediaDetached, this);
this.hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this);
}
private _unregisterListeners () {
private _unregisterListeners() {
this.hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
this.hls.off(Events.MEDIA_DETACHED, this.onMediaDetached, this);
this.hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this);
@@ -152,27 +147,27 @@ class EMEController implements ComponentAPI {
* @returns {string} License server URL for key-system (if any configured, otherwise causes error)
* @throws if a unsupported keysystem is passed
*/
getLicenseServerUrl (keySystem: KeySystems): string {
getLicenseServerUrl(keySystem: KeySystems): string {
switch (keySystem) {
case KeySystems.WIDEVINE:
if (!this._widevineLicenseUrl) {
break;
}
return this._widevineLicenseUrl;
case KeySystems.WIDEVINE:
if (!this._widevineLicenseUrl) {
break;
}
return this._widevineLicenseUrl;
}
throw new Error(`no license server URL configured for key-system "${keySystem}"`);
}
/**
* Requests access object and adds it to our list upon success
* @private
* @param {string} keySystem System ID (see `KeySystems`)
* @param {Array<string>} audioCodecs List of required audio codecs to support
* @param {Array<string>} videoCodecs List of required video codecs to support
* @throws When a unsupported KeySystem is passed
*/
private _attemptKeySystemAccess (keySystem: KeySystems, audioCodecs: string[], videoCodecs: string[]) {
* Requests access object and adds it to our list upon success
* @private
* @param {string} keySystem System ID (see `KeySystems`)
* @param {Array<string>} audioCodecs List of required audio codecs to support
* @param {Array<string>} videoCodecs List of required video codecs to support
* @throws When a unsupported KeySystem is passed
*/
private _attemptKeySystemAccess(keySystem: KeySystems, audioCodecs: string[], videoCodecs: string[]) {
// This can throw, but is caught in event handler callpath
const mediaKeySystemConfigs = getSupportedMediaKeySystemConfigurations(keySystem, audioCodecs, videoCodecs, this._drmSystemOptions);
@@ -181,15 +176,14 @@ class EMEController implements ComponentAPI {
// expecting interface like window.navigator.requestMediaKeySystemAccess
const keySystemAccessPromise = this.requestMediaKeySystemAccess(keySystem, mediaKeySystemConfigs);
this.mediaKeysPromise = keySystemAccessPromise.then((mediaKeySystemAccess) =>
this._onMediaKeySystemAccessObtained(keySystem, mediaKeySystemAccess));
this.mediaKeysPromise = keySystemAccessPromise.then((mediaKeySystemAccess) => this._onMediaKeySystemAccessObtained(keySystem, mediaKeySystemAccess));
keySystemAccessPromise.catch((err) => {
logger.error(`Failed to obtain key-system "${keySystem}" access:`, err);
});
}
get requestMediaKeySystemAccess () {
get requestMediaKeySystemAccess() {
if (!this._requestMediaKeySystemAccess) {
throw new Error('No requestMediaKeySystemAccess function configured');
}
@@ -198,23 +192,24 @@ class EMEController implements ComponentAPI {
}
/**
* Handles obtaining access to a key-system
* @private
* @param {string} keySystem
* @param {MediaKeySystemAccess} mediaKeySystemAccess https://developer.mozilla.org/en-US/docs/Web/API/MediaKeySystemAccess
*/
private _onMediaKeySystemAccessObtained (keySystem: KeySystems, mediaKeySystemAccess: MediaKeySystemAccess): Promise<MediaKeys> {
* Handles obtaining access to a key-system
* @private
* @param {string} keySystem
* @param {MediaKeySystemAccess} mediaKeySystemAccess https://developer.mozilla.org/en-US/docs/Web/API/MediaKeySystemAccess
*/
private _onMediaKeySystemAccessObtained(keySystem: KeySystems, mediaKeySystemAccess: MediaKeySystemAccess): Promise<MediaKeys> {
logger.log(`Access for key-system "${keySystem}" obtained`);
const mediaKeysListItem: MediaKeysListItem = {
mediaKeysSessionInitialized: false,
mediaKeySystemAccess: mediaKeySystemAccess,
mediaKeySystemDomain: keySystem
mediaKeySystemDomain: keySystem,
};
this._mediaKeysList.push(mediaKeysListItem);
const mediaKeysPromise = Promise.resolve().then(() => mediaKeySystemAccess.createMediaKeys())
const mediaKeysPromise = Promise.resolve()
.then(() => mediaKeySystemAccess.createMediaKeys())
.then((mediaKeys) => {
mediaKeysListItem.mediaKeys = mediaKeys;
@@ -238,7 +233,7 @@ class EMEController implements ComponentAPI {
*
* @private
*/
private _onMediaKeysCreated () {
private _onMediaKeysCreated() {
// check for all key-list items if a session exists, otherwise, create one
this._mediaKeysList.forEach((mediaKeysListItem) => {
if (!mediaKeysListItem.mediaKeysSession) {
@@ -250,15 +245,19 @@ class EMEController implements ComponentAPI {
}
/**
* @private
* @param {*} keySession
*/
private _onNewMediaKeySession (keySession: MediaKeySession) {
* @private
* @param {*} keySession
*/
private _onNewMediaKeySession(keySession: MediaKeySession) {
logger.log(`New key-system session ${keySession.sessionId}`);
keySession.addEventListener('message', (event: MediaKeyMessageEvent) => {
this._onKeySessionMessage(keySession, event.message);
}, false);
keySession.addEventListener(
'message',
(event: MediaKeyMessageEvent) => {
this._onKeySessionMessage(keySession, event.message);
},
false
);
}
/**
@@ -266,7 +265,7 @@ class EMEController implements ComponentAPI {
* @param {MediaKeySession} keySession
* @param {ArrayBuffer} message
*/
private _onKeySessionMessage (keySession: MediaKeySession, message: ArrayBuffer) {
private _onKeySessionMessage(keySession: MediaKeySession, message: ArrayBuffer) {
logger.log('Got EME message event, creating license request');
this._requestLicense(message, (data: ArrayBuffer) => {
@@ -287,7 +286,7 @@ class EMEController implements ComponentAPI {
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.KEY_SYSTEM_ERROR,
details: ErrorDetails.KEY_SYSTEM_NO_KEYS,
fatal: true
fatal: true,
});
return;
}
@@ -302,12 +301,12 @@ class EMEController implements ComponentAPI {
// Could use `Promise.finally` but some Promise polyfills are missing it
this.mediaKeysPromise.then(finallySetKeyAndStartSession).catch(finallySetKeyAndStartSession);
}
};
/**
* @private
*/
private _attemptSetMediaKeys (mediaKeys?: MediaKeys) {
private _attemptSetMediaKeys(mediaKeys?: MediaKeys) {
if (!this._media) {
throw new Error('Attempted to set mediaKeys without first attaching a media element');
}
@@ -320,7 +319,7 @@ class EMEController implements ComponentAPI {
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.KEY_SYSTEM_ERROR,
details: ErrorDetails.KEY_SYSTEM_NO_KEYS,
fatal: true
fatal: true,
});
return;
}
@@ -335,7 +334,7 @@ class EMEController implements ComponentAPI {
/**
* @private
*/
private _generateRequestWithPreferredKeySession (initDataType: string, initData: ArrayBuffer | null) {
private _generateRequestWithPreferredKeySession(initDataType: string, initData: ArrayBuffer | null) {
// FIXME: see if we can/want/need-to really to deal with several potential key-sessions?
const keysListItem = this._mediaKeysList[0];
if (!keysListItem) {
@@ -343,7 +342,7 @@ class EMEController implements ComponentAPI {
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.KEY_SYSTEM_ERROR,
details: ErrorDetails.KEY_SYSTEM_NO_ACCESS,
fatal: true
fatal: true,
});
return;
}
@@ -359,7 +358,7 @@ class EMEController implements ComponentAPI {
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.KEY_SYSTEM_ERROR,
details: ErrorDetails.KEY_SYSTEM_NO_SESSION,
fatal: true
fatal: true,
});
return;
}
@@ -370,7 +369,7 @@ class EMEController implements ComponentAPI {
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.KEY_SYSTEM_ERROR,
details: ErrorDetails.KEY_SYSTEM_NO_INIT_DATA,
fatal: true
fatal: true,
});
return;
}
@@ -378,7 +377,8 @@ class EMEController implements ComponentAPI {
logger.log(`Generating key-session request for "${initDataType}" init data type`);
keysListItem.mediaKeysSessionInitialized = true;
keySession.generateRequest(initDataType, initData)
keySession
.generateRequest(initDataType, initData)
.then(() => {
logger.debug('Key-session generation succeeded');
})
@@ -387,7 +387,7 @@ class EMEController implements ComponentAPI {
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.KEY_SYSTEM_ERROR,
details: ErrorDetails.KEY_SYSTEM_NO_SESSION,
fatal: false
fatal: false,
});
});
}
@@ -400,7 +400,7 @@ class EMEController implements ComponentAPI {
* @returns {XMLHttpRequest} Unsent (but opened state) XHR object
* @throws if XMLHttpRequest construction failed
*/
private _createLicenseXhr (url: string, keyMessage: ArrayBuffer, callback: (data: ArrayBuffer) => void): XMLHttpRequest {
private _createLicenseXhr(url: string, keyMessage: ArrayBuffer, callback: (data: ArrayBuffer) => void): XMLHttpRequest {
const xhr = new XMLHttpRequest();
const licenseXhrSetup = this._licenseXhrSetup;
@@ -425,8 +425,7 @@ class EMEController implements ComponentAPI {
// Because we set responseType to ArrayBuffer here, callback is typed as handling only array buffers
xhr.responseType = 'arraybuffer';
xhr.onreadystatechange =
this._onLicenseRequestReadyStageChange.bind(this, xhr, url, keyMessage, callback);
xhr.onreadystatechange = this._onLicenseRequestReadyStageChange.bind(this, xhr, url, keyMessage, callback);
return xhr;
}
@@ -437,34 +436,34 @@ class EMEController implements ComponentAPI {
* @param {ArrayBuffer} keyMessage Message data issued by key-system
* @param {function} callback Called when XHR has succeeded
*/
private _onLicenseRequestReadyStageChange (xhr: XMLHttpRequest, url: string, keyMessage: ArrayBuffer, callback: (data: ArrayBuffer) => void) {
private _onLicenseRequestReadyStageChange(xhr: XMLHttpRequest, url: string, keyMessage: ArrayBuffer, callback: (data: ArrayBuffer) => void) {
switch (xhr.readyState) {
case 4:
if (xhr.status === 200) {
this._requestLicenseFailureCount = 0;
logger.log('License request succeeded');
case 4:
if (xhr.status === 200) {
this._requestLicenseFailureCount = 0;
logger.log('License request succeeded');
if (xhr.responseType !== 'arraybuffer') {
logger.warn('xhr response type was not set to the expected arraybuffer for license request');
}
callback(xhr.response);
} else {
logger.error(`License Request XHR failed (${url}). Status: ${xhr.status} (${xhr.statusText})`);
this._requestLicenseFailureCount++;
if (this._requestLicenseFailureCount > MAX_LICENSE_REQUEST_FAILURES) {
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.KEY_SYSTEM_ERROR,
details: ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED,
fatal: true
});
return;
}
if (xhr.responseType !== 'arraybuffer') {
logger.warn('xhr response type was not set to the expected arraybuffer for license request');
}
callback(xhr.response);
} else {
logger.error(`License Request XHR failed (${url}). Status: ${xhr.status} (${xhr.statusText})`);
this._requestLicenseFailureCount++;
if (this._requestLicenseFailureCount > MAX_LICENSE_REQUEST_FAILURES) {
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.KEY_SYSTEM_ERROR,
details: ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED,
fatal: true,
});
return;
}
const attemptsLeft = MAX_LICENSE_REQUEST_FAILURES - this._requestLicenseFailureCount + 1;
logger.warn(`Retrying license request, ${attemptsLeft} attempts left`);
this._requestLicense(keyMessage, callback);
}
break;
const attemptsLeft = MAX_LICENSE_REQUEST_FAILURES - this._requestLicenseFailureCount + 1;
logger.warn(`Retrying license request, ${attemptsLeft} attempts left`);
this._requestLicense(keyMessage, callback);
}
break;
}
}
@@ -475,11 +474,11 @@ class EMEController implements ComponentAPI {
* @returns {ArrayBuffer} Challenge data posted to license server
* @throws if KeySystem is unsupported
*/
private _generateLicenseRequestChallenge (keysListItem: MediaKeysListItem, keyMessage: ArrayBuffer): ArrayBuffer {
private _generateLicenseRequestChallenge(keysListItem: MediaKeysListItem, keyMessage: ArrayBuffer): ArrayBuffer {
switch (keysListItem.mediaKeySystemDomain) {
// case KeySystems.PLAYREADY:
// from https://github.com/MicrosoftEdge/Demos/blob/master/eme/scripts/demo.js
/*
// case KeySystems.PLAYREADY:
// from https://github.com/MicrosoftEdge/Demos/blob/master/eme/scripts/demo.js
/*
if (this.licenseType !== this.LICENSE_TYPE_WIDEVINE) {
// For PlayReady CDMs, we need to dig the Challenge out of the XML.
var keyMessageXml = new DOMParser().parseFromString(String.fromCharCode.apply(null, new Uint16Array(keyMessage)), 'application/xml');
@@ -499,9 +498,9 @@ class EMEController implements ComponentAPI {
}
break;
*/
case KeySystems.WIDEVINE:
// For Widevine CDMs, the challenge is the keyMessage.
return keyMessage;
case KeySystems.WIDEVINE:
// For Widevine CDMs, the challenge is the keyMessage.
return keyMessage;
}
throw new Error(`unsupported key-system: ${keysListItem.mediaKeySystemDomain}`);
@@ -512,7 +511,7 @@ class EMEController implements ComponentAPI {
* @param keyMessage
* @param callback
*/
private _requestLicense (keyMessage: ArrayBuffer, callback: (data: ArrayBuffer) => void) {
private _requestLicense(keyMessage: ArrayBuffer, callback: (data: ArrayBuffer) => void) {
logger.log('Requesting content license for key-system');
const keysListItem = this._mediaKeysList[0];
@@ -521,7 +520,7 @@ class EMEController implements ComponentAPI {
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.KEY_SYSTEM_ERROR,
details: ErrorDetails.KEY_SYSTEM_NO_ACCESS,
fatal: true
fatal: true,
});
return;
}
@@ -537,12 +536,12 @@ class EMEController implements ComponentAPI {
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.KEY_SYSTEM_ERROR,
details: ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED,
fatal: true
fatal: true,
});
}
}
onMediaAttached (event: Events.MEDIA_ATTACHED, data: MediaAttachedData) {
onMediaAttached(event: Events.MEDIA_ATTACHED, data: MediaAttachedData) {
if (!this._emeEnabled) {
return;
}
@@ -555,7 +554,7 @@ class EMEController implements ComponentAPI {
media.addEventListener('encrypted', this._onMediaEncrypted);
}
onMediaDetached () {
onMediaDetached() {
const media = this._media;
const mediaKeysList = this._mediaKeysList;
if (!media) {
@@ -565,31 +564,31 @@ class EMEController implements ComponentAPI {
this._media = null;
this._mediaKeysList = [];
// Close all sessions and remove media keys from the video element.
Promise.all(mediaKeysList.map((mediaKeysListItem) => {
if (mediaKeysListItem.mediaKeysSession) {
return mediaKeysListItem.mediaKeysSession.close().catch(() => {
// Ignore errors when closing the sessions. Closing a session that
// generated no key requests will throw an error.
});
}
})).then(() => {
return media.setMediaKeys(null);
}).catch(() => {
// Ignore any failures while removing media keys from the video element.
});
Promise.all(
mediaKeysList.map((mediaKeysListItem) => {
if (mediaKeysListItem.mediaKeysSession) {
return mediaKeysListItem.mediaKeysSession.close().catch(() => {
// Ignore errors when closing the sessions. Closing a session that
// generated no key requests will throw an error.
});
}
})
)
.then(() => {
return media.setMediaKeys(null);
})
.catch(() => {
// Ignore any failures while removing media keys from the video element.
});
}
onManifestParsed (event: Events.MANIFEST_PARSED, data: ManifestParsedData) {
onManifestParsed(event: Events.MANIFEST_PARSED, data: ManifestParsedData) {
if (!this._emeEnabled) {
return;
}
const audioCodecs = data.levels.map((level) => level.audioCodec).filter(
(audioCodec: string | undefined): audioCodec is string => !!audioCodec
);
const videoCodecs = data.levels.map((level) => level.videoCodec).filter(
(videoCodec: string | undefined): videoCodec is string => !!videoCodec
);
const audioCodecs = data.levels.map((level) => level.audioCodec).filter((audioCodec: string | undefined): audioCodec is string => !!audioCodec);
const videoCodecs = data.levels.map((level) => level.videoCodec).filter((videoCodec: string | undefined): videoCodec is string => !!videoCodec);
this._attemptKeySystemAccess(KeySystems.WIDEVINE, audioCodecs, videoCodecs);
}
+9 -9
View File
@@ -16,25 +16,25 @@ class FPSController implements ComponentAPI {
// stream controller must be provided as a dependency!
private streamController!: StreamController;
constructor (hls: Hls) {
constructor(hls: Hls) {
this.hls = hls;
this.registerListeners();
}
public setStreamController (streamController: StreamController) {
public setStreamController(streamController: StreamController) {
this.streamController = streamController;
}
protected registerListeners () {
protected registerListeners() {
this.hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
}
protected unregisterListeners () {
protected unregisterListeners() {
this.hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching);
}
destroy () {
destroy() {
if (this.timer) {
clearInterval(this.timer);
}
@@ -44,7 +44,7 @@ class FPSController implements ComponentAPI {
this.media = null;
}
protected onMediaAttaching (event: Events.MEDIA_ATTACHING, data: MediaAttachingData) {
protected onMediaAttaching(event: Events.MEDIA_ATTACHING, data: MediaAttachingData) {
const config = this.hls.config;
if (config.capLevelOnFPSDrop) {
const media = data.media instanceof self.HTMLVideoElement ? data.media : null;
@@ -58,14 +58,14 @@ class FPSController implements ComponentAPI {
}
}
checkFPS (video: HTMLVideoElement, decodedFrames: number, droppedFrames: number) {
checkFPS(video: HTMLVideoElement, decodedFrames: number, droppedFrames: number) {
const currentTime = performance.now();
if (decodedFrames) {
if (this.lastTime) {
const currentPeriod = currentTime - this.lastTime;
const currentDropped = droppedFrames - this.lastDroppedFrames;
const currentDecoded = decodedFrames - this.lastDecodedFrames;
const droppedFPS = 1000 * currentDropped / currentPeriod;
const droppedFPS = (1000 * currentDropped) / currentPeriod;
const hls = this.hls;
hls.trigger(Events.FPS_DROP, { currentDropped: currentDropped, currentDecoded: currentDecoded, totalDroppedFrames: droppedFrames });
if (droppedFPS > 0) {
@@ -88,7 +88,7 @@ class FPSController implements ComponentAPI {
}
}
checkFPSInterval () {
checkFPSInterval() {
const video = this.media;
if (video) {
if (this.isVideoPlaybackQualityAvailable) {
+6 -6
View File
@@ -8,7 +8,7 @@ import Fragment from '../loader/fragment';
* @param {number} [maxFragLookUpTolerance = 0] - The amount of time that a fragment's start/end can be within in order to be considered contiguous
* @returns {*|null} fragment - The best matching fragment
*/
export function findFragmentByPDT (fragments: Array<Fragment>, PDTValue: number | null, maxFragLookUpTolerance: number): Fragment | null {
export function findFragmentByPDT(fragments: Array<Fragment>, PDTValue: number | null, maxFragLookUpTolerance: number): Fragment | null {
if (PDTValue === null || !Array.isArray(fragments) || !fragments.length || !Number.isFinite(PDTValue)) {
return null;
}
@@ -45,10 +45,10 @@ export function findFragmentByPDT (fragments: Array<Fragment>, PDTValue: number
* @param {number} maxFragLookUpTolerance - The amount of time that a fragment's start/end can be within in order to be considered contiguous
* @returns {*} foundFrag - The best matching fragment
*/
export function findFragmentByPTS (fragPrevious: Fragment | null, fragments: Array<Fragment>, bufferEnd: number = 0, maxFragLookUpTolerance: number = 0): Fragment | null {
export function findFragmentByPTS(fragPrevious: Fragment | null, fragments: Array<Fragment>, bufferEnd: number = 0, maxFragLookUpTolerance: number = 0): Fragment | null {
let fragNext: Fragment | null = null;
if (fragPrevious) {
fragNext = fragments[fragPrevious.sn as number - (fragments[0].sn as number) + 1];
fragNext = fragments[(fragPrevious.sn as number) - (fragments[0].sn as number) + 1];
} else if (bufferEnd === 0 && fragments[0].start === 0) {
fragNext = fragments[0];
}
@@ -72,7 +72,7 @@ export function findFragmentByPTS (fragPrevious: Fragment | null, fragments: Arr
* @param {number} [maxFragLookUpTolerance = 0] - The amount of time that a fragment's start can be within in order to be considered contiguous
* @returns {number} - 0 if it matches, 1 if too low, -1 if too high
*/
export function fragmentWithinToleranceTest (bufferEnd = 0, maxFragLookUpTolerance = 0, candidate: Fragment) {
export function fragmentWithinToleranceTest(bufferEnd = 0, maxFragLookUpTolerance = 0, candidate: Fragment) {
// offset should be within fragment boundary - config.maxFragLookUpTolerance
// this is to cope with situations like
// bufferEnd = 9.991
@@ -106,7 +106,7 @@ export function fragmentWithinToleranceTest (bufferEnd = 0, maxFragLookUpToleran
* @param {number} [maxFragLookUpTolerance = 0] - The amount of time that a fragment's start can be within in order to be considered contiguous
* @returns {boolean} True if contiguous, false otherwise
*/
export function pdtWithinToleranceTest (pdtBufferEnd: number, maxFragLookUpTolerance: number, candidate: Fragment): boolean {
export function pdtWithinToleranceTest(pdtBufferEnd: number, maxFragLookUpTolerance: number, candidate: Fragment): boolean {
const candidateLookupTolerance = Math.min(maxFragLookUpTolerance, candidate.duration + (candidate.deltaPTS ? candidate.deltaPTS : 0)) * 1000;
// endProgramDateTime can be null, default to zero
@@ -114,7 +114,7 @@ export function pdtWithinToleranceTest (pdtBufferEnd: number, maxFragLookUpToler
return endProgramDateTime - candidateLookupTolerance > pdtBufferEnd;
}
export function findFragWithCC (fragments, CC): Fragment | null {
export function findFragWithCC(fragments, CC): Fragment | null {
return BinarySearch.search(fragments, (candidate) => {
if (candidate.cc < CC) {
return 1;
+37 -37
View File
@@ -12,41 +12,43 @@ export enum FragmentState {
BACKTRACKED = 'BACKTRACKED',
APPENDING = 'APPENDING',
PARTIAL = 'PARTIAL',
OK = 'OK'
OK = 'OK',
}
export class FragmentTracker implements ComponentAPI {
private activeFragment: Fragment | null = null;
private activePart: Part | null = null;
private fragments: Partial<Record<string, FragmentEntity>> = Object.create(null);
private timeRanges: {
[key in SourceBufferName]: TimeRanges
} | null = Object.create(null);
private timeRanges:
| {
[key in SourceBufferName]: TimeRanges;
}
| null = Object.create(null);
private bufferPadding: number = 0.2;
private hls: Hls;
constructor (hls: Hls) {
constructor(hls: Hls) {
this.hls = hls;
this._registerListeners();
}
private _registerListeners () {
private _registerListeners() {
const { hls } = this;
hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this);
hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
hls.on(Events.FRAG_LOADED, this.onFragLoaded, this);
}
private _unregisterListeners () {
private _unregisterListeners() {
const { hls } = this;
hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this);
hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
hls.off(Events.FRAG_LOADED, this.onFragLoaded, this);
}
public destroy () {
public destroy() {
this.fragments = Object.create(null);
this.timeRanges = Object.create(null);
this._unregisterListeners();
@@ -56,7 +58,7 @@ export class FragmentTracker implements ComponentAPI {
* Return a Fragment with an appended range that matches the position and levelType.
* If not found any Fragment, return null
*/
public getAppendedFrag (position: number, levelType: PlaylistLevelType) : Fragment | null {
public getAppendedFrag(position: number, levelType: PlaylistLevelType): Fragment | null {
const { activeFragment } = this;
if (!activeFragment) {
return null;
@@ -72,10 +74,10 @@ export class FragmentTracker implements ComponentAPI {
* A buffered Fragment is one whose loading, parsing and appending is done (completed or "partial" meaning aborted).
* If not found any Fragment, return null
*/
public getBufferedFrag (position: number, levelType: PlaylistLevelType) : Fragment | null {
public getBufferedFrag(position: number, levelType: PlaylistLevelType): Fragment | null {
const { fragments } = this;
const keys = Object.keys(fragments);
for (let i = keys.length; i--;) {
for (let i = keys.length; i--; ) {
const fragmentEntity = fragments[keys[i]];
if (fragmentEntity?.body.type === levelType && fragmentEntity.buffered) {
const frag = fragmentEntity.body;
@@ -92,9 +94,9 @@ export class FragmentTracker implements ComponentAPI {
* The browser will unload parts of the buffer to free up memory for new buffer data
* Fragments will need to be reloaded when the buffer is freed up, removing partial fragments will allow them to reload(since there might be parts that are still playable)
*/
public detectEvictedFragments (elementaryStream: SourceBufferName, timeRange: TimeRanges) {
public detectEvictedFragments(elementaryStream: SourceBufferName, timeRange: TimeRanges) {
// Check if any flagged fragments have been unloaded
Object.keys(this.fragments).forEach(key => {
Object.keys(this.fragments).forEach((key) => {
const fragmentEntity = this.fragments[key];
if (!fragmentEntity || !fragmentEntity.buffered) {
return;
@@ -118,7 +120,7 @@ export class FragmentTracker implements ComponentAPI {
* Checks if the fragment passed in is loaded in the buffer properly
* Partially loaded fragments will be registered as a partial fragment
*/
private detectPartialFragments (data: FragBufferedData) {
private detectPartialFragments(data: FragBufferedData) {
const timeRanges = this.timeRanges;
const { frag, part } = data;
if (!timeRanges || frag.sn === 'initSegment') {
@@ -132,7 +134,7 @@ export class FragmentTracker implements ComponentAPI {
}
fragmentEntity.buffered = true;
fragmentEntity.backtrack = fragmentEntity.loaded = null;
Object.keys(timeRanges).forEach(elementaryStream => {
Object.keys(timeRanges).forEach((elementaryStream) => {
const streamInfo = frag.elementaryStreams[elementaryStream];
if (!streamInfo) {
return;
@@ -143,10 +145,10 @@ export class FragmentTracker implements ComponentAPI {
});
}
private getBufferedTimes (fragment: Fragment, part: Part | null, partial: boolean, timeRange: TimeRanges): FragmentBufferedRange {
private getBufferedTimes(fragment: Fragment, part: Part | null, partial: boolean, timeRange: TimeRanges): FragmentBufferedRange {
const buffered: FragmentBufferedRange = {
time: [],
partial
partial,
};
const startPTS = part ? part.start : fragment.start;
const endPTS = part ? part.end : fragment.end;
@@ -160,7 +162,7 @@ export class FragmentTracker implements ComponentAPI {
// No need to check the other timeRange times since it's completely playable
buffered.time.push({
startPTS: Math.max(startPTS, timeRange.start(i)),
endPTS: Math.min(endPTS, timeRange.end(i))
endPTS: Math.min(endPTS, timeRange.end(i)),
});
break;
} else if (startPTS < endTime && endPTS > startTime) {
@@ -169,7 +171,7 @@ export class FragmentTracker implements ComponentAPI {
// Get playable sections of the fragment
buffered.time.push({
startPTS: Math.max(startPTS, timeRange.start(i)),
endPTS: Math.min(endPTS, timeRange.end(i))
endPTS: Math.min(endPTS, timeRange.end(i)),
});
} else if (endPTS <= startTime) {
// No need to check the rest of the timeRange as it is in order
@@ -182,14 +184,14 @@ export class FragmentTracker implements ComponentAPI {
/**
* Gets the partial fragment for a certain time
*/
public getPartialFragment (time: number): Fragment | null {
public getPartialFragment(time: number): Fragment | null {
let bestFragment: Fragment | null = null;
let timePadding: number;
let startTime: number;
let endTime: number;
let bestOverlap: number = 0;
const { bufferPadding, fragments } = this;
Object.keys(fragments).forEach(key => {
Object.keys(fragments).forEach((key) => {
const fragmentEntity = fragments[key];
if (!fragmentEntity) {
return;
@@ -210,7 +212,7 @@ export class FragmentTracker implements ComponentAPI {
return bestFragment;
}
public getState (fragment: Fragment): FragmentState {
public getState(fragment: Fragment): FragmentState {
const fragKey = getFragmentKey(fragment);
const fragmentEntity = this.fragments[fragKey];
@@ -230,7 +232,7 @@ export class FragmentTracker implements ComponentAPI {
return FragmentState.NOT_LOADED;
}
public backtrack (data: FragLoadedData) {
public backtrack(data: FragLoadedData) {
const { frag } = data;
const fragKey = getFragmentKey(frag);
const fragmentEntity = this.fragments[fragKey];
@@ -241,7 +243,7 @@ export class FragmentTracker implements ComponentAPI {
fragmentEntity.loaded = null;
}
public getBacktrackData (fragment: Fragment): FragLoadedData | null {
public getBacktrackData(fragment: Fragment): FragLoadedData | null {
const fragKey = getFragmentKey(fragment);
const fragmentEntity = this.fragments[fragKey];
if (fragmentEntity) {
@@ -250,7 +252,7 @@ export class FragmentTracker implements ComponentAPI {
return null;
}
private isTimeBuffered (startPTS: number, endPTS: number, timeRange: TimeRanges): boolean {
private isTimeBuffered(startPTS: number, endPTS: number, timeRange: TimeRanges): boolean {
let startTime;
let endTime;
for (let i = 0; i < timeRange.length; i++) {
@@ -269,7 +271,7 @@ export class FragmentTracker implements ComponentAPI {
return false;
}
private onFragLoaded (event: Events.FRAG_LOADED, data: FragLoadedData) {
private onFragLoaded(event: Events.FRAG_LOADED, data: FragLoadedData) {
const { frag, part } = data;
// don't track initsegment (for which sn is not a number)
// don't track frags used for bitrateTest, they're irrelevant.
@@ -284,11 +286,11 @@ export class FragmentTracker implements ComponentAPI {
loaded: data,
backtrack: null,
buffered: false,
range: Object.create(null)
range: Object.create(null),
};
}
private onBufferAppended (event: Events.BUFFER_APPENDED, data: BufferAppendedData) {
private onBufferAppended(event: Events.BUFFER_APPENDED, data: BufferAppendedData) {
const { frag, part, timeRanges } = data;
this.activeFragment = frag;
this.activePart = part;
@@ -305,33 +307,31 @@ export class FragmentTracker implements ComponentAPI {
});
}
private onFragBuffered (event: Events.FRAG_BUFFERED, data: FragBufferedData) {
private onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
this.detectPartialFragments(data);
}
private hasFragment (fragment: Fragment): boolean {
private hasFragment(fragment: Fragment): boolean {
const fragKey = getFragmentKey(fragment);
return !!this.fragments[fragKey];
}
public removeFragment (fragment: Fragment) {
public removeFragment(fragment: Fragment) {
const fragKey = getFragmentKey(fragment);
fragment.stats.loaded = 0;
fragment.clearElementaryStreamInfo();
delete this.fragments[fragKey];
}
public removeAllFragments () {
public removeAllFragments() {
this.fragments = Object.create(null);
}
}
function isPartial (fragmentEntity: FragmentEntity): boolean {
return fragmentEntity.buffered &&
(fragmentEntity.range.video?.partial ||
fragmentEntity.range.audio?.partial);
function isPartial(fragmentEntity: FragmentEntity): boolean {
return fragmentEntity.buffered && (fragmentEntity.range.video?.partial || fragmentEntity.range.audio?.partial);
}
function getFragmentKey (fragment: Fragment): string {
function getFragmentKey(fragment: Fragment): string {
return `${fragment.type}_${fragment.level}_${fragment.urlId}_${fragment.sn}`;
}
+12 -14
View File
@@ -24,7 +24,7 @@ export default class GapController {
private moved: boolean = false;
private seeking: boolean = false;
constructor (config, media, fragmentTracker, hls) {
constructor(config, media, fragmentTracker, hls) {
this.config = config;
this.media = media;
this.fragmentTracker = fragmentTracker;
@@ -37,7 +37,7 @@ export default class GapController {
*
* @param {number} lastCurrentTime Previously read playhead position
*/
public poll (lastCurrentTime: number) {
public poll(lastCurrentTime: number) {
const { config, media, stalled } = this;
const { currentTime, seeking } = media;
const seeked = this.seeking && !seeking;
@@ -84,8 +84,7 @@ export default class GapController {
// Waiting for seeking in a buffered range to complete
const hasEnoughBuffer = bufferInfo.len > MAX_START_GAP_JUMP;
// Next buffered range is too far ahead to jump to while still seeking
const noBufferGap = !nextStart ||
(nextStart - currentTime > MAX_START_GAP_JUMP && !this.fragmentTracker.getPartialFragment(currentTime));
const noBufferGap = !nextStart || (nextStart - currentTime > MAX_START_GAP_JUMP && !this.fragmentTracker.getPartialFragment(currentTime));
if (hasEnoughBuffer || noBufferGap) {
return;
}
@@ -134,7 +133,7 @@ export default class GapController {
* @param stalledDurationMs - The amount of time Hls.js has been stalling for.
* @private
*/
private _tryFixBufferStall (bufferInfo: BufferInfo, stalledDurationMs: number) {
private _tryFixBufferStall(bufferInfo: BufferInfo, stalledDurationMs: number) {
const { config, fragmentTracker, media } = this;
const currentTime = media.currentTime;
@@ -154,8 +153,7 @@ export default class GapController {
// we may just have to "nudge" the playlist as the browser decoding/rendering engine
// needs to cross some sort of threshold covering all source-buffers content
// to start playing properly.
if (bufferInfo.len > config.maxBufferHole &&
stalledDurationMs > config.highBufferWatchdogPeriod * 1000) {
if (bufferInfo.len > config.maxBufferHole && stalledDurationMs > config.highBufferWatchdogPeriod * 1000) {
logger.warn('Trying to nudge playhead over buffer-hole');
// Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds
// We only try to jump the hole if it's under the configured size
@@ -170,7 +168,7 @@ export default class GapController {
* @param bufferLen - The playhead distance from the end of the current buffer segment.
* @private
*/
private _reportStall (bufferLen) {
private _reportStall(bufferLen) {
const { hls, media, stallReported } = this;
if (!stallReported) {
// Report stalled error once
@@ -180,7 +178,7 @@ export default class GapController {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.BUFFER_STALLED_ERROR,
fatal: false,
buffer: bufferLen
buffer: bufferLen,
});
}
}
@@ -190,7 +188,7 @@ export default class GapController {
* @param partial - The partial fragment found at the current time (where playback is stalling).
* @private
*/
private _trySkipBufferHole (partial: Fragment | null): number {
private _trySkipBufferHole(partial: Fragment | null): number {
const { config, hls, media } = this;
const currentTime = media.currentTime;
let lastEndTime = 0;
@@ -210,7 +208,7 @@ export default class GapController {
details: ErrorDetails.BUFFER_SEEK_OVER_HOLE,
fatal: false,
reason: `fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`,
frag: partial
frag: partial,
});
}
return targetTime;
@@ -224,7 +222,7 @@ export default class GapController {
* Attempts to fix buffer stalls by advancing the mediaElement's current time by a small amount.
* @private
*/
private _tryNudgeBuffer () {
private _tryNudgeBuffer() {
const { config, hls, media } = this;
const currentTime = media.currentTime;
const nudgeRetry = (this.nudgeRetry || 0) + 1;
@@ -238,14 +236,14 @@ export default class GapController {
hls.trigger(Events.ERROR, {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.BUFFER_NUDGE_ON_STALL,
fatal: false
fatal: false,
});
} else {
logger.error(`Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`);
hls.trigger(Events.ERROR, {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.BUFFER_STALLED_ERROR,
fatal: true
fatal: true,
});
}
}
+9 -9
View File
@@ -18,16 +18,16 @@ class ID3TrackController implements ComponentAPI {
private id3Track: TextTrack | null = null;
private media: HTMLMediaElement | null = null;
constructor (hls) {
constructor(hls) {
this.hls = hls;
this._registerListeners();
}
destroy () {
destroy() {
this._unregisterListeners();
}
private _registerListeners () {
private _registerListeners() {
const { hls } = this;
hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -35,7 +35,7 @@ class ID3TrackController implements ComponentAPI {
hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
}
private _unregisterListeners () {
private _unregisterListeners() {
const { hls } = this;
hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -44,11 +44,11 @@ class ID3TrackController implements ComponentAPI {
}
// Add ID3 metatadata text track.
protected onMediaAttached (event: Events.MEDIA_ATTACHED, data: MediaAttachedData): void {
protected onMediaAttached(event: Events.MEDIA_ATTACHED, data: MediaAttachedData): void {
this.media = data.media;
}
protected onMediaDetaching (): void {
protected onMediaDetaching(): void {
if (!this.id3Track) {
return;
}
@@ -57,7 +57,7 @@ class ID3TrackController implements ComponentAPI {
this.media = null;
}
getID3Track (textTracks: TextTrackList): TextTrack | void {
getID3Track(textTracks: TextTrackList): TextTrack | void {
if (!this.media) {
return;
}
@@ -74,7 +74,7 @@ class ID3TrackController implements ComponentAPI {
return this.media.addTextTrack('metadata', 'id3');
}
onFragParsingMetadata (event: Events.FRAG_PARSING_METADATA, data: FragParsingMetadataData) {
onFragParsingMetadata(event: Events.FRAG_PARSING_METADATA, data: FragParsingMetadataData) {
if (!this.media) {
return;
}
@@ -116,7 +116,7 @@ class ID3TrackController implements ComponentAPI {
}
}
onBufferFlushing (event: Events.BUFFER_FLUSHING, { startOffset, endOffset, type }: BufferFlushingData) {
onBufferFlushing(event: Events.BUFFER_FLUSHING, { startOffset, endOffset, type }: BufferFlushingData) {
if (!type || type === 'audio') {
// id3 cues come from parsed audio only remove cues when audio buffer is cleared
const { id3Track } = this;
+18 -18
View File
@@ -17,17 +17,17 @@ export default class LatencyController implements ComponentAPI {
private _latency: number | null = null;
private timeupdateHandler = () => this.timeupdate();
constructor (hls: Hls) {
constructor(hls: Hls) {
this.hls = hls;
this.config = hls.config;
this.registerListeners();
}
get latency (): number {
get latency(): number {
return this._latency || 0;
}
get maxLatency (): number {
get maxLatency(): number {
const { config, levelDetails } = this;
if (config.liveMaxLatencyDuration !== undefined) {
return config.liveMaxLatencyDuration;
@@ -35,7 +35,7 @@ export default class LatencyController implements ComponentAPI {
return levelDetails ? config.liveMaxLatencyDurationCount * levelDetails.targetduration : 0;
}
get targetLatency (): number | null {
get targetLatency(): number | null {
const { levelDetails } = this;
if (levelDetails === null) {
return null;
@@ -52,7 +52,7 @@ export default class LatencyController implements ComponentAPI {
return targetLatency + Math.min(this.stallCount * liveSyncOnStallIncrease, maxLiveSyncOnStallIncrease);
}
get liveSyncPosition (): number | null {
get liveSyncPosition(): number | null {
const liveEdge = this.estimateLiveEdge();
const targetLatency = this.targetLatency;
if (liveEdge === null || targetLatency === null || this.levelDetails === null) {
@@ -61,7 +61,7 @@ export default class LatencyController implements ComponentAPI {
return Math.min(this.levelDetails.edge, liveEdge - targetLatency - this.edgeStalled);
}
get edgeStalled (): number {
get edgeStalled(): number {
const { levelDetails } = this;
if (levelDetails === null) {
return 0;
@@ -70,7 +70,7 @@ export default class LatencyController implements ComponentAPI {
return Math.max(levelDetails.age - maxLevelUpdateAge, 0);
}
private get forwardBufferLength (): number {
private get forwardBufferLength(): number {
const { media, levelDetails } = this;
if (!media || !levelDetails) {
return 0;
@@ -79,12 +79,12 @@ export default class LatencyController implements ComponentAPI {
return bufferedRanges ? media.buffered.end(bufferedRanges - 1) : levelDetails.edge - this.currentTime;
}
public destroy (): void {
public destroy(): void {
this.unregisterListeners();
this.onMediaDetaching();
}
private registerListeners () {
private registerListeners() {
this.hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
this.hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
this.hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
@@ -92,7 +92,7 @@ export default class LatencyController implements ComponentAPI {
this.hls.on(Events.ERROR, this.onError, this);
}
private unregisterListeners () {
private unregisterListeners() {
this.hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached);
this.hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching);
this.hls.off(Events.MANIFEST_LOADING, this.onManifestLoading);
@@ -100,25 +100,25 @@ export default class LatencyController implements ComponentAPI {
this.hls.off(Events.ERROR, this.onError);
}
private onMediaAttached (event: Events.MEDIA_ATTACHED, data: MediaAttachingData) {
private onMediaAttached(event: Events.MEDIA_ATTACHED, data: MediaAttachingData) {
this.media = data.media;
this.media.addEventListener('timeupdate', this.timeupdateHandler);
}
private onMediaDetaching () {
private onMediaDetaching() {
if (this.media) {
this.media.removeEventListener('timeupdate', this.timeupdateHandler);
this.media = null;
}
}
private onManifestLoading () {
private onManifestLoading() {
this.levelDetails = null;
this._latency = null;
this.stallCount = 0;
}
private onLevelUpdated (event: Events.LEVEL_UPDATED, { details }: LevelUpdatedData) {
private onLevelUpdated(event: Events.LEVEL_UPDATED, { details }: LevelUpdatedData) {
this.levelDetails = details;
if (details.advanced) {
this.timeupdate();
@@ -128,7 +128,7 @@ export default class LatencyController implements ComponentAPI {
}
}
private onError (event: Events.ERROR, data: ErrorData) {
private onError(event: Events.ERROR, data: ErrorData) {
if (data.details !== ErrorDetails.BUFFER_STALLED_ERROR) {
return;
}
@@ -136,7 +136,7 @@ export default class LatencyController implements ComponentAPI {
logger.warn('[playback-rate-controller]: Stall detected, adjusting target latency');
}
private timeupdate () {
private timeupdate() {
const { media, levelDetails } = this;
if (!media || !levelDetails) {
return;
@@ -173,7 +173,7 @@ export default class LatencyController implements ComponentAPI {
}
}
private estimateLiveEdge (): number | null {
private estimateLiveEdge(): number | null {
const { levelDetails } = this;
if (levelDetails === null) {
return null;
@@ -181,7 +181,7 @@ export default class LatencyController implements ComponentAPI {
return levelDetails.edge + levelDetails.age;
}
private computeLatency (): number | null {
private computeLatency(): number | null {
const liveEdge = this.estimateLiveEdge();
if (liveEdge === null) {
return null;
+92 -97
View File
@@ -1,15 +1,8 @@
/*
* Level Controller
*/
*/
import {
ManifestLoadedData,
ManifestParsedData,
LevelLoadedData,
TrackSwitchedData,
FragLoadedData,
ErrorData, LevelSwitchingData
} from '../types/events';
import { ManifestLoadedData, ManifestParsedData, LevelLoadedData, TrackSwitchedData, FragLoadedData, ErrorData, LevelSwitchingData } from '../types/events';
import { Level } from '../types/level';
import { Events } from '../events';
import { ErrorTypes, ErrorDetails } from '../errors';
@@ -33,12 +26,12 @@ export default class LevelController extends BasePlaylistController {
public onParsedComplete!: Function;
constructor (hls: Hls) {
constructor(hls: Hls) {
super(hls, '[level-controller]');
this._registerListeners();
}
private _registerListeners () {
private _registerListeners() {
const { hls } = this;
hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this);
@@ -47,7 +40,7 @@ export default class LevelController extends BasePlaylistController {
hls.on(Events.ERROR, this.onError, this);
}
private _unregisterListeners () {
private _unregisterListeners() {
const { hls } = this;
hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this);
hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this);
@@ -56,29 +49,29 @@ export default class LevelController extends BasePlaylistController {
hls.off(Events.ERROR, this.onError, this);
}
public destroy () {
public destroy() {
super.destroy();
this._unregisterListeners();
this.manualLevelIndex = -1;
}
public startLoad (): void {
public startLoad(): void {
const levels = this._levels;
// clean up live level details to force reload them, and reset load errors
levels.forEach(level => {
levels.forEach((level) => {
level.loadError = 0;
});
super.startLoad();
}
protected onManifestLoaded (event: Events.MANIFEST_LOADED, data: ManifestLoadedData): void {
protected onManifestLoaded(event: Events.MANIFEST_LOADED, data: ManifestLoadedData): void {
let levels: Level[] = [];
let audioTracks: MediaPlaylist[] = [];
let subtitleTracks: MediaPlaylist[] = [];
let bitrateStart: number | undefined;
const levelSet: { [bitrate: number]: Level; } = {};
const levelSet: { [bitrate: number]: Level } = {};
let levelFromSet: Level;
let videoCodecFound = false;
let audioCodecFound = false;
@@ -127,7 +120,7 @@ export default class LevelController extends BasePlaylistController {
});
if (data.audioTracks) {
audioTracks = data.audioTracks.filter(track => !track.audioCodec || isCodecSupportedInMp4(track.audioCodec, 'audio'));
audioTracks = data.audioTracks.filter((track) => !track.audioCodec || isCodecSupportedInMp4(track.audioCodec, 'audio'));
// Assign ids after filtering as array indices by group-id
assignTrackIdsByGroup(audioTracks);
}
@@ -163,7 +156,7 @@ export default class LevelController extends BasePlaylistController {
stats: data.stats,
audio: audioCodecFound,
video: videoCodecFound,
altAudio: !audioOnly && audioTracks.some(t => !!t.url)
altAudio: !audioOnly && audioTracks.some((t) => !!t.url),
};
this.hls.trigger(Events.MANIFEST_PARSED, edata);
@@ -174,23 +167,23 @@ export default class LevelController extends BasePlaylistController {
details: ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR,
fatal: true,
url: data.url,
reason: 'no level with compatible codecs found in manifest'
reason: 'no level with compatible codecs found in manifest',
});
}
}
get levels (): Level[] | null {
get levels(): Level[] | null {
if (this._levels.length === 0) {
return null;
}
return this._levels;
}
get level (): number {
get level(): number {
return this.currentLevelIndex;
}
set level (newLevel: number) {
set level(newLevel: number) {
const levels = this._levels;
if (this.currentLevelIndex === newLevel && levels[newLevel]?.details) {
return;
@@ -203,7 +196,7 @@ export default class LevelController extends BasePlaylistController {
details: ErrorDetails.LEVEL_SWITCH_ERROR,
level: newLevel,
fatal: false,
reason: 'invalid level idx'
reason: 'invalid level idx',
});
return;
}
@@ -220,7 +213,7 @@ export default class LevelController extends BasePlaylistController {
level: newLevel,
maxBitrate: level.maxBitrate,
uri: level.uri,
urlId: level.urlId
urlId: level.urlId,
});
// @ts-ignore
delete levelSwitchingData._urlId;
@@ -234,11 +227,11 @@ export default class LevelController extends BasePlaylistController {
}
}
get manualLevel (): number {
get manualLevel(): number {
return this.manualLevelIndex;
}
set manualLevel (newLevel) {
set manualLevel(newLevel) {
this.manualLevelIndex = newLevel;
if (this._startLevel === undefined) {
this._startLevel = newLevel;
@@ -249,15 +242,15 @@ export default class LevelController extends BasePlaylistController {
}
}
get firstLevel (): number {
get firstLevel(): number {
return this._firstLevel;
}
set firstLevel (newLevel) {
set firstLevel(newLevel) {
this._firstLevel = newLevel;
}
get startLevel () {
get startLevel() {
// hls.startLevel takes precedence over config.startLevel
// if none of these values are defined, fallback on this._firstLevel (first quality level appearing in variant manifest)
if (this._startLevel === undefined) {
@@ -272,11 +265,11 @@ export default class LevelController extends BasePlaylistController {
}
}
set startLevel (newLevel) {
set startLevel(newLevel) {
this._startLevel = newLevel;
}
protected onError (event: Events.ERROR, data: ErrorData) {
protected onError(event: Events.ERROR, data: ErrorData) {
super.onError(event, data);
if (data.fatal) {
return;
@@ -285,9 +278,11 @@ export default class LevelController extends BasePlaylistController {
// Switch to redundant level when track fails to load
const context = data.context;
const level = this._levels[this.currentLevelIndex];
if (context &&
if (
context &&
((context.type === PlaylistContextType.AUDIO_TRACK && level.audioGroupIds && context.groupId === level.audioGroupIds[level.urlId]) ||
(context.type === PlaylistContextType.SUBTITLE_TRACK && level.textGroupIds && context.groupId === level.textGroupIds[level.urlId]))) {
(context.type === PlaylistContextType.SUBTITLE_TRACK && level.textGroupIds && context.groupId === level.textGroupIds[level.urlId]))
) {
this.redundantFailover(this.currentLevelIndex);
return;
}
@@ -299,32 +294,32 @@ export default class LevelController extends BasePlaylistController {
// try to recover not fatal errors
switch (data.details) {
case ErrorDetails.FRAG_LOAD_ERROR:
case ErrorDetails.FRAG_LOAD_TIMEOUT:
case ErrorDetails.KEY_LOAD_ERROR:
case ErrorDetails.KEY_LOAD_TIMEOUT:
// FIXME: What distinguishes these fragment events from level or track fragments?
// We shouldn't recover a level if the fragment or key is for a media track
console.assert(data.frag, 'Event has a fragment defined.');
levelIndex = (data.frag as Fragment).level;
fragmentError = true;
break;
case ErrorDetails.LEVEL_LOAD_ERROR:
case ErrorDetails.LEVEL_LOAD_TIMEOUT:
// Do not perform level switch if an error occurred using delivery directives
// Attempt to reload level without directives first
if (context) {
if (context.deliveryDirectives) {
levelSwitch = false;
case ErrorDetails.FRAG_LOAD_ERROR:
case ErrorDetails.FRAG_LOAD_TIMEOUT:
case ErrorDetails.KEY_LOAD_ERROR:
case ErrorDetails.KEY_LOAD_TIMEOUT:
// FIXME: What distinguishes these fragment events from level or track fragments?
// We shouldn't recover a level if the fragment or key is for a media track
console.assert(data.frag, 'Event has a fragment defined.');
levelIndex = (data.frag as Fragment).level;
fragmentError = true;
break;
case ErrorDetails.LEVEL_LOAD_ERROR:
case ErrorDetails.LEVEL_LOAD_TIMEOUT:
// Do not perform level switch if an error occurred using delivery directives
// Attempt to reload level without directives first
if (context) {
if (context.deliveryDirectives) {
levelSwitch = false;
}
levelIndex = context.level;
}
levelIndex = context.level;
}
levelError = true;
break;
case ErrorDetails.REMUX_ALLOC_ERROR:
levelIndex = data.level;
levelError = true;
break;
levelError = true;
break;
case ErrorDetails.REMUX_ALLOC_ERROR:
levelIndex = data.level;
levelError = true;
break;
}
if (levelIndex !== undefined) {
@@ -336,7 +331,7 @@ export default class LevelController extends BasePlaylistController {
* Switch to a redundant stream if any available.
* If redundant stream is not available, emergency switch down if ABR mode is enabled.
*/
private recoverLevel (errorEvent: ErrorData, levelIndex: number, levelError: boolean, fragmentError: boolean, levelSwitch: boolean): void {
private recoverLevel(errorEvent: ErrorData, levelIndex: number, levelError: boolean, fragmentError: boolean, levelSwitch: boolean): void {
const { details: errorDetails } = errorEvent;
const level = this._levels[levelIndex];
@@ -365,7 +360,7 @@ export default class LevelController extends BasePlaylistController {
// Search for available level
if (this.manualLevelIndex === -1) {
// When lowest level has been reached, let's start hunt from the top
const nextLevel = (levelIndex === 0) ? this._levels.length - 1 : levelIndex - 1;
const nextLevel = levelIndex === 0 ? this._levels.length - 1 : levelIndex - 1;
if (this.currentLevelIndex !== nextLevel) {
fragmentError = false;
this.warn(`${errorDetails}: switch to ${nextLevel}`);
@@ -382,14 +377,14 @@ export default class LevelController extends BasePlaylistController {
}
}
private redundantFailover (levelIndex: number) {
private redundantFailover(levelIndex: number) {
const level = this._levels[levelIndex];
const redundantLevels = level.url.length;
if (redundantLevels > 1) {
// Update the url id of all levels so that we stay on the same set of variants when level switching
const newUrlId = (level.urlId + 1) % redundantLevels;
this.warn(`Switching to redundant URL-id ${newUrlId}`);
this._levels.forEach(level => {
this._levels.forEach((level) => {
level.urlId = newUrlId;
});
this.level = levelIndex;
@@ -397,7 +392,7 @@ export default class LevelController extends BasePlaylistController {
}
// reset errors on the successful load of a fragment
protected onFragLoaded (event: Events.FRAG_LOADED, { frag }: FragLoadedData) {
protected onFragLoaded(event: Events.FRAG_LOADED, { frag }: FragLoadedData) {
if (frag !== undefined && frag.type === 'main') {
const level = this._levels[frag.level];
if (level !== undefined) {
@@ -407,7 +402,7 @@ export default class LevelController extends BasePlaylistController {
}
}
protected onLevelLoaded (event: Events.LEVEL_LOADED, data: LevelLoadedData) {
protected onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
const { level, details } = data;
const curLevel = this._levels[level];
@@ -434,7 +429,7 @@ export default class LevelController extends BasePlaylistController {
}
}
protected onAudioTrackSwitched (event: Events.AUDIO_TRACK_SWITCHED, data: TrackSwitchedData) {
protected onAudioTrackSwitched(event: Events.AUDIO_TRACK_SWITCHED, data: TrackSwitchedData) {
const currentLevel = this.hls.levels[this.currentLevelIndex];
if (!currentLevel) {
return;
@@ -457,7 +452,7 @@ export default class LevelController extends BasePlaylistController {
}
}
protected loadPlaylist (hlsUrlParameters?: HlsUrlParameters) {
protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters) {
const level = this.currentLevelIndex;
const currentLevel = this._levels[level];
@@ -472,9 +467,7 @@ export default class LevelController extends BasePlaylistController {
}
}
this.log(`Attempt loading level index ${level}${
hlsUrlParameters ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part : ''
} with URL-id ${id} ${url}`);
this.log(`Attempt loading level index ${level}${hlsUrlParameters ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part : ''} with URL-id ${id} ${url}`);
// console.log('Current audio track group ID:', this.hls.audioTracks[this.hls.audioTrack].groupId);
// console.log('New video quality level audio group id:', levelObject.attrs.AUDIO, level);
@@ -483,12 +476,12 @@ export default class LevelController extends BasePlaylistController {
url,
level,
id,
deliveryDirectives: hlsUrlParameters || null
deliveryDirectives: hlsUrlParameters || null,
});
}
}
get nextLoadLevel () {
get nextLoadLevel() {
if (this.manualLevelIndex !== -1) {
return this.manualLevelIndex;
} else {
@@ -496,41 +489,43 @@ export default class LevelController extends BasePlaylistController {
}
}
set nextLoadLevel (nextLevel) {
set nextLoadLevel(nextLevel) {
this.level = nextLevel;
if (this.manualLevelIndex === -1) {
this.hls.nextAutoLevel = nextLevel;
}
}
removeLevel (levelIndex, urlId) {
removeLevel(levelIndex, urlId) {
const filterLevelAndGroupByIdIndex = (url, id) => id !== urlId;
const levels = this._levels.filter((level, index) => {
if (index !== levelIndex) {
return true;
}
const levels = this._levels
.filter((level, index) => {
if (index !== levelIndex) {
return true;
}
if (level.url.length > 1 && urlId !== undefined) {
level.url = level.url.filter(filterLevelAndGroupByIdIndex);
if (level.audioGroupIds) {
level.audioGroupIds = level.audioGroupIds.filter(filterLevelAndGroupByIdIndex);
if (level.url.length > 1 && urlId !== undefined) {
level.url = level.url.filter(filterLevelAndGroupByIdIndex);
if (level.audioGroupIds) {
level.audioGroupIds = level.audioGroupIds.filter(filterLevelAndGroupByIdIndex);
}
if (level.textGroupIds) {
level.textGroupIds = level.textGroupIds.filter(filterLevelAndGroupByIdIndex);
}
level.urlId = 0;
return true;
}
if (level.textGroupIds) {
level.textGroupIds = level.textGroupIds.filter(filterLevelAndGroupByIdIndex);
return false;
})
.map((level, index) => {
const { details } = level;
if (details?.fragments) {
details.fragments.forEach((fragment) => {
fragment.level = index;
});
}
level.urlId = 0;
return true;
}
return false;
}).map((level, index) => {
const { details } = level;
if (details?.fragments) {
details.fragments.forEach((fragment) => {
fragment.level = index;
});
}
return level;
});
return level;
});
this._levels = levels;
this.hls.trigger(Events.LEVELS_UPDATED, { levels });
+28 -29
View File
@@ -13,24 +13,24 @@ import type { MediaPlaylist } from '../types/media-playlist';
type FragmentIntersection = (oldFrag: Fragment, newFrag: Fragment) => void;
type PartIntersection = (oldPart: Part, newPart: Part) => void;
export function addGroupId (level: Level, type: string, id: string): void {
export function addGroupId(level: Level, type: string, id: string): void {
switch (type) {
case 'audio':
if (!level.audioGroupIds) {
level.audioGroupIds = [];
}
level.audioGroupIds.push(id);
break;
case 'text':
if (!level.textGroupIds) {
level.textGroupIds = [];
}
level.textGroupIds.push(id);
break;
case 'audio':
if (!level.audioGroupIds) {
level.audioGroupIds = [];
}
level.audioGroupIds.push(id);
break;
case 'text':
if (!level.textGroupIds) {
level.textGroupIds = [];
}
level.textGroupIds.push(id);
break;
}
}
export function assignTrackIdsByGroup (tracks: MediaPlaylist[]): void {
export function assignTrackIdsByGroup(tracks: MediaPlaylist[]): void {
const groups = {};
tracks.forEach((track) => {
const groupId = track.groupId || '';
@@ -39,13 +39,13 @@ export function assignTrackIdsByGroup (tracks: MediaPlaylist[]): void {
});
}
export function updatePTS (fragments: Fragment[], fromIdx: number, toIdx: number): void {
export function updatePTS(fragments: Fragment[], fromIdx: number, toIdx: number): void {
const fragFrom = fragments[fromIdx];
const fragTo = fragments[toIdx];
updateFromToPTS(fragFrom, fragTo);
}
function updateFromToPTS (fragFrom: Fragment, fragTo: Fragment) {
function updateFromToPTS(fragFrom: Fragment, fragTo: Fragment) {
const fragToPTS = fragTo.startPTS as number;
// if we know startPTS[toIdx]
if (Number.isFinite(fragToPTS)) {
@@ -80,7 +80,7 @@ function updateFromToPTS (fragFrom: Fragment, fragTo: Fragment) {
}
}
export function updateFragPTSDTS (details: LevelDetails | undefined, frag: Fragment, startPTS: number, endPTS: number, startDTS: number, endDTS: number): number {
export function updateFragPTSDTS(details: LevelDetails | undefined, frag: Fragment, startPTS: number, endPTS: number, startDTS: number, endDTS: number): number {
const parsedMediaDuration = endPTS - startPTS;
if (parsedMediaDuration <= 0) {
logger.warn('Fragment should have a positive duration', frag);
@@ -150,7 +150,7 @@ export function updateFragPTSDTS (details: LevelDetails | undefined, frag: Fragm
return drift;
}
export function mergeDetails (oldDetails: LevelDetails, newDetails: LevelDetails): void {
export function mergeDetails(oldDetails: LevelDetails, newDetails: LevelDetails): void {
// potentially retrieve cached initsegment
if (newDetails.initSegment && oldDetails.initSegment) {
newDetails.initSegment = oldDetails.initSegment;
@@ -191,10 +191,10 @@ export function mergeDetails (oldDetails: LevelDetails, newDetails: LevelDetails
});
if (newDetails.skippedSegments) {
newDetails.deltaUpdateFailed = newDetails.fragments.some(frag => !frag);
newDetails.deltaUpdateFailed = newDetails.fragments.some((frag) => !frag);
if (newDetails.deltaUpdateFailed) {
logger.warn('[level-helper] Previous playlist missing segments skipped in delta playlist');
for (let i = newDetails.skippedSegments; i--;) {
for (let i = newDetails.skippedSegments; i--; ) {
newDetails.fragments.shift();
}
newDetails.startSN = newDetails.fragments[0].sn as number;
@@ -237,7 +237,7 @@ export function mergeDetails (oldDetails: LevelDetails, newDetails: LevelDetails
}
}
export function mapPartIntersection (oldParts: Part[] | null, newParts: Part[] | null, intersectionFn: PartIntersection) {
export function mapPartIntersection(oldParts: Part[] | null, newParts: Part[] | null, intersectionFn: PartIntersection) {
if (oldParts && newParts) {
let delta = 0;
for (let i = 0, len = oldParts.length; i <= len; i++) {
@@ -252,11 +252,10 @@ export function mapPartIntersection (oldParts: Part[] | null, newParts: Part[] |
}
}
export function mapFragmentIntersection (oldDetails: LevelDetails, newDetails: LevelDetails, intersectionFn: FragmentIntersection): void {
export function mapFragmentIntersection(oldDetails: LevelDetails, newDetails: LevelDetails, intersectionFn: FragmentIntersection): void {
const skippedSegments = newDetails.skippedSegments;
const start = Math.max(oldDetails.startSN, newDetails.startSN) - newDetails.startSN;
const end = (oldDetails.fragmentHint ? 1 : 0) +
(skippedSegments ? newDetails.endSN : Math.min(oldDetails.endSN, newDetails.endSN)) - newDetails.startSN;
const end = (oldDetails.fragmentHint ? 1 : 0) + (skippedSegments ? newDetails.endSN : Math.min(oldDetails.endSN, newDetails.endSN)) - newDetails.startSN;
const delta = newDetails.startSN - oldDetails.startSN;
const newFrags = newDetails.fragmentHint ? newDetails.fragments.concat(newDetails.fragmentHint) : newDetails.fragments;
const oldFrags = oldDetails.fragmentHint ? oldDetails.fragments.concat(oldDetails.fragmentHint) : oldDetails.fragments;
@@ -274,7 +273,7 @@ export function mapFragmentIntersection (oldDetails: LevelDetails, newDetails: L
}
}
export function adjustSliding (oldDetails: LevelDetails, newDetails: LevelDetails): void {
export function adjustSliding(oldDetails: LevelDetails, newDetails: LevelDetails): void {
const delta = newDetails.startSN + newDetails.skippedSegments - oldDetails.startSN;
const oldFragments = oldDetails.fragments;
const newFragments = newDetails.fragments;
@@ -292,7 +291,7 @@ export function adjustSliding (oldDetails: LevelDetails, newDetails: LevelDetail
}
}
export function computeReloadInterval (newDetails: LevelDetails, stats: LoaderStats): number {
export function computeReloadInterval(newDetails: LevelDetails, stats: LoaderStats): number {
const reloadInterval = 1000 * newDetails.levelTargetDuration;
const reloadIntervalAfterMiss = reloadInterval / 2;
const timeSinceLastModified = newDetails.age;
@@ -339,7 +338,7 @@ export function computeReloadInterval (newDetails: LevelDetails, stats: LoaderSt
return Math.round(estimatedTimeUntilUpdate);
}
export function getFragmentWithSN (level: Level, sn: number): Fragment | null {
export function getFragmentWithSN(level: Level, sn: number): Fragment | null {
if (!level || !level.details) {
return null;
}
@@ -355,13 +354,13 @@ export function getFragmentWithSN (level: Level, sn: number): Fragment | null {
return null;
}
export function getPartWith (level: Level, sn: number, partIndex: number): Part | null {
export function getPartWith(level: Level, sn: number, partIndex: number): Part | null {
if (!level || !level.details) {
return null;
}
const partList = level.details.partList;
if (partList) {
for (let i = partList.length; i--;) {
for (let i = partList.length; i--; ) {
const part = partList[i];
if (part.index === partIndex && part.fragment.sn === sn) {
return part;
+171 -182
View File
@@ -32,7 +32,7 @@ import type {
FragParsingUserdataData,
FragBufferedData,
BufferFlushedData,
ErrorData
ErrorData,
} from '../types/events';
const TICK_INTERVAL = 100; // how often to tick in ms
@@ -56,7 +56,7 @@ export default class StreamController extends BaseStreamController implements Ne
private audioCodecSwitch: boolean = false;
private videoBuffer: any | null = null;
constructor (hls: Hls, fragmentTracker: FragmentTracker) {
constructor(hls: Hls, fragmentTracker: FragmentTracker) {
super(hls, fragmentTracker, '[stream-controller]');
this.fragmentLoader = new FragmentLoader(hls.config);
this.state = State.STOPPED;
@@ -64,7 +64,7 @@ export default class StreamController extends BaseStreamController implements Ne
this._registerListeners();
}
private _registerListeners () {
private _registerListeners() {
const { hls } = this;
hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -82,7 +82,7 @@ export default class StreamController extends BaseStreamController implements Ne
hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
}
protected _unregisterListeners () {
protected _unregisterListeners() {
const { hls } = this;
hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -99,11 +99,11 @@ export default class StreamController extends BaseStreamController implements Ne
hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
}
protected onHandlerDestroying () {
protected onHandlerDestroying() {
this._unregisterListeners();
}
startLoad (startPosition: number): void {
startLoad(startPosition: number): void {
if (this.levels) {
const { lastCurrentTime, hls } = this;
this.stopLoad();
@@ -141,53 +141,54 @@ export default class StreamController extends BaseStreamController implements Ne
}
}
stopLoad () {
stopLoad() {
this._forceStartLoad = false;
super.stopLoad();
}
doTick () {
doTick() {
switch (this.state) {
case State.IDLE:
this.doTickIdle();
break;
case State.WAITING_LEVEL: {
const { levels, level } = this;
const details = levels?.[level]?.details;
if (details && (!details.live || this.levelLastLoaded === this.level)) {
if (this.waitForCdnTuneIn(details)) {
case State.IDLE:
this.doTickIdle();
break;
case State.WAITING_LEVEL: {
const { levels, level } = this;
const details = levels?.[level]?.details;
if (details && (!details.live || this.levelLastLoaded === this.level)) {
if (this.waitForCdnTuneIn(details)) {
break;
}
this.state = State.IDLE;
break;
}
this.state = State.IDLE;
break;
}
break;
}
case State.FRAG_LOADING_WAITING_RETRY: {
const now = self.performance.now();
const retryDate = this.retryDate;
// if current time is gt than retryDate, or if media seeking let's switch to IDLE state to retry loading
if (!retryDate || (now >= retryDate) || this.media?.seeking) {
this.log('retryDate reached, switch back to IDLE state');
this.state = State.IDLE;
}
}
break;
default:
break;
case State.FRAG_LOADING_WAITING_RETRY:
{
const now = self.performance.now();
const retryDate = this.retryDate;
// if current time is gt than retryDate, or if media seeking let's switch to IDLE state to retry loading
if (!retryDate || now >= retryDate || this.media?.seeking) {
this.log('retryDate reached, switch back to IDLE state');
this.state = State.IDLE;
}
}
break;
default:
break;
}
// check buffer
// check/update current fragment
this.onTickEnd();
}
protected onTickEnd () {
protected onTickEnd() {
super.onTickEnd();
this.checkBuffer();
this.checkFragmentChanged();
}
private doTickIdle () {
private doTickIdle() {
const { hls, levelLastLoaded, levels, media } = this;
const { config, nextLoadLevel: level } = hls;
@@ -234,7 +235,7 @@ export default class StreamController extends BaseStreamController implements Ne
const levelBitrate = levelInfo.maxBitrate;
let maxBufLen;
if (levelBitrate) {
maxBufLen = Math.max(8 * config.maxBufferSize / levelBitrate, config.maxBufferLength);
maxBufLen = Math.max((8 * config.maxBufferSize) / levelBitrate, config.maxBufferLength);
} else {
maxBufLen = config.maxBufferLength;
}
@@ -283,12 +284,12 @@ export default class StreamController extends BaseStreamController implements Ne
}
}
private loadKey (frag: Fragment) {
private loadKey(frag: Fragment) {
this.state = State.KEY_LOADING;
this.hls.trigger(Events.KEY_LOADING, { frag });
}
protected loadFragment (frag: Fragment, levelDetails: LevelDetails, targetBufferTime: number) {
protected loadFragment(frag: Fragment, levelDetails: LevelDetails, targetBufferTime: number) {
// Check if fragment is not loaded
const fragState = this.fragmentTracker.getState(frag);
this.fragCurrent = frag;
@@ -328,15 +329,15 @@ export default class StreamController extends BaseStreamController implements Ne
}
}
getAppendedFrag (position) {
getAppendedFrag(position) {
return this.fragmentTracker.getAppendedFrag(position, PlaylistLevelType.MAIN);
}
getBufferedFrag (position) {
getBufferedFrag(position) {
return this.fragmentTracker.getBufferedFrag(position, PlaylistLevelType.MAIN);
}
followingBufferedFrag (frag: Fragment | null) {
followingBufferedFrag(frag: Fragment | null) {
if (frag) {
// try to get range of next fragment (500ms after this range)
return this.getBufferedFrag(frag.end + 0.5);
@@ -350,7 +351,7 @@ export default class StreamController extends BaseStreamController implements Ne
- cancel any pending load request
- and trigger a buffer flush
*/
immediateLevelSwitch () {
immediateLevelSwitch() {
this.log('immediateLevelSwitch');
if (!this.immediateSwitch) {
this.immediateSwitch = true;
@@ -382,7 +383,7 @@ export default class StreamController extends BaseStreamController implements Ne
* - nudge video decoder by slightly adjusting video currentTime (if currentTime buffered)
* - resume the playback if needed
*/
immediateLevelSwitchEnd () {
immediateLevelSwitchEnd() {
const media = this.media;
if (BufferHelper.getBuffered(media).length) {
this.immediateSwitch = false;
@@ -402,7 +403,7 @@ export default class StreamController extends BaseStreamController implements Ne
* we need to find the next flushable buffer range
* we should take into account new segment fetch time
*/
nextLevelSwitch () {
nextLevelSwitch() {
const { levels, media } = this;
// ensure that media is defined and that metadata are available (to retrieve currentTime)
if (media?.readyState) {
@@ -419,7 +420,7 @@ export default class StreamController extends BaseStreamController implements Ne
const nextLevel = levels[nextLevelId];
const fragLastKbps = this.fragLastKbps;
if (fragLastKbps && this.fragCurrent) {
fetchdelay = this.fragCurrent.duration * nextLevel.maxBitrate / (1000 * fragLastKbps) + 1;
fetchdelay = (this.fragCurrent.duration * nextLevel.maxBitrate) / (1000 * fragLastKbps) + 1;
} else {
fetchdelay = 0;
}
@@ -451,11 +452,11 @@ export default class StreamController extends BaseStreamController implements Ne
}
}
flushMainBuffer (startOffset: number, endOffset: number) {
flushMainBuffer(startOffset: number, endOffset: number) {
super.flushMainBuffer(startOffset, endOffset, this.altAudio ? 'video' : null);
}
onMediaAttached (event: Events.MEDIA_ATTACHED, data: MediaAttachedData) {
onMediaAttached(event: Events.MEDIA_ATTACHED, data: MediaAttachedData) {
super.onMediaAttached(event, data);
const media = data.media;
this.onvplaying = this.onMediaPlaying.bind(this);
@@ -465,7 +466,7 @@ export default class StreamController extends BaseStreamController implements Ne
this.gapController = new GapController(this.config, media, this.fragmentTracker, this.hls);
}
onMediaDetaching () {
onMediaDetaching() {
const { media } = this;
if (media) {
media.removeEventListener('playing', this.onvplaying);
@@ -476,12 +477,12 @@ export default class StreamController extends BaseStreamController implements Ne
super.onMediaDetaching();
}
onMediaPlaying () {
onMediaPlaying() {
// tick to speed up FRAG_CHANGED triggering
this.tick();
}
onMediaSeeked () {
onMediaSeeked() {
const media = this.media;
const currentTime = media ? media.currentTime : null;
if (Number.isFinite(currentTime)) {
@@ -492,7 +493,7 @@ export default class StreamController extends BaseStreamController implements Ne
this.tick();
}
onManifestLoading () {
onManifestLoading() {
// reset buffer on manifest loading
this.log('Trigger BUFFER_RESET');
this.hls.trigger(Events.BUFFER_RESET, undefined);
@@ -502,11 +503,11 @@ export default class StreamController extends BaseStreamController implements Ne
this.fragPlaying = null;
}
onManifestParsed (event: Events.MANIFEST_PARSED, data: ManifestParsedData) {
onManifestParsed(event: Events.MANIFEST_PARSED, data: ManifestParsedData) {
let aac = false;
let heaac = false;
let codec;
data.levels.forEach(level => {
data.levels.forEach((level) => {
// detect if we have different kind of audio codecs used amongst playlists
codec = level.audioCodec;
if (codec) {
@@ -519,7 +520,7 @@ export default class StreamController extends BaseStreamController implements Ne
}
}
});
this.audioCodecSwitch = (aac && heaac);
this.audioCodecSwitch = aac && heaac;
if (this.audioCodecSwitch) {
this.log('Both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC');
}
@@ -528,7 +529,7 @@ export default class StreamController extends BaseStreamController implements Ne
this.startFragRequested = false;
}
onLevelLoading (event: Events.LEVEL_LOADING, data: LevelLoadingData) {
onLevelLoading(event: Events.LEVEL_LOADING, data: LevelLoadingData) {
const { levels } = this;
if (!levels || this.state !== State.IDLE) {
return;
@@ -539,7 +540,7 @@ export default class StreamController extends BaseStreamController implements Ne
}
}
onLevelLoaded (event: Events.LEVEL_LOADED, data: LevelLoadedData) {
onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
const { levels } = this;
const newLevelId = data.level;
const newDetails = data.details;
@@ -595,7 +596,7 @@ export default class StreamController extends BaseStreamController implements Ne
this.tick();
}
_handleFragmentLoadProgress (data: FragLoadedData) {
_handleFragmentLoadProgress(data: FragLoadedData) {
const { frag, part, payload } = data;
const { levels } = this;
if (!levels) {
@@ -617,35 +618,24 @@ export default class StreamController extends BaseStreamController implements Ne
// transmux the MPEG-TS data to ISO-BMFF segments
// this.log(`Transmuxing ${frag.sn} of [${details.startSN} ,${details.endSN}],level ${frag.level}, cc ${frag.cc}`);
const transmuxer = this.transmuxer = this.transmuxer ||
new TransmuxerInterface(this.hls, PlaylistLevelType.MAIN, this._handleTransmuxComplete.bind(this), this._handleTransmuxerFlush.bind(this));
const transmuxer = (this.transmuxer =
this.transmuxer || new TransmuxerInterface(this.hls, PlaylistLevelType.MAIN, this._handleTransmuxComplete.bind(this), this._handleTransmuxerFlush.bind(this)));
const partIndex = part ? part.index : -1;
const partial = partIndex !== -1;
const chunkMeta = new ChunkMetadata(frag.level, frag.sn as number, frag.stats.chunkCount, payload.byteLength, partIndex, partial);
const initPTS = this.initPTS[frag.cc];
transmuxer.push(
payload,
initSegmentData,
audioCodec,
videoCodec,
frag,
part,
details.totalduration,
accurateTimeOffset,
chunkMeta,
initPTS
);
transmuxer.push(payload, initSegmentData, audioCodec, videoCodec, frag, part, details.totalduration, accurateTimeOffset, chunkMeta, initPTS);
}
private resetTransmuxer () {
private resetTransmuxer() {
if (this.transmuxer) {
this.transmuxer.destroy();
this.transmuxer = null;
}
}
onAudioTrackSwitching (event: Events.AUDIO_TRACK_SWITCHING, data: AudioTrackSwitchingData) {
onAudioTrackSwitching(event: Events.AUDIO_TRACK_SWITCHING, data: AudioTrackSwitchingData) {
// if any URL found on new audio track, it is an alternate audio track
const fromAltAudio = this.altAudio;
const altAudio = !!data.url;
@@ -679,16 +669,16 @@ export default class StreamController extends BaseStreamController implements Ne
hls.trigger(Events.BUFFER_FLUSHING, {
startOffset: 0,
endOffset: Number.POSITIVE_INFINITY,
type: 'audio'
type: 'audio',
});
}
hls.trigger(Events.AUDIO_TRACK_SWITCHED, {
id: trackId
id: trackId,
});
}
}
onAudioTrackSwitched (event: Events.AUDIO_TRACK_SWITCHED, data: AudioTrackSwitchedData) {
onAudioTrackSwitched(event: Events.AUDIO_TRACK_SWITCHED, data: AudioTrackSwitchedData) {
const trackId = data.id;
const altAudio = !!this.hls.audioTracks[trackId].url;
if (altAudio) {
@@ -703,7 +693,7 @@ export default class StreamController extends BaseStreamController implements Ne
this.tick();
}
onBufferCreated (event: Events.BUFFER_CREATED, data: BufferCreatedData) {
onBufferCreated(event: Events.BUFFER_CREATED, data: BufferCreatedData) {
const tracks = data.tracks;
let mediaTrack;
let name;
@@ -732,7 +722,7 @@ export default class StreamController extends BaseStreamController implements Ne
}
}
onFragBuffered (event: Events.FRAG_BUFFERED, data: FragBufferedData) {
onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
const { frag, part } = data;
if (frag && frag.type !== 'main') {
return;
@@ -744,12 +734,12 @@ export default class StreamController extends BaseStreamController implements Ne
return;
}
const stats = part ? part.stats : frag.stats;
this.fragLastKbps = Math.round(8 * stats.total / (stats.buffering.end - stats.loading.first));
this.fragLastKbps = Math.round((8 * stats.total) / (stats.buffering.end - stats.loading.first));
this.fragPrevious = frag;
this.fragBufferedComplete(frag, part);
}
onError (event: Events.ERROR, data: ErrorData) {
onError(event: Events.ERROR, data: ErrorData) {
const frag = data.frag || this.fragCurrent;
// don't handle frag error not related to main fragment
if (frag && frag.type !== 'main') {
@@ -760,74 +750,74 @@ export default class StreamController extends BaseStreamController implements Ne
const mediaBuffered = !!this.media && BufferHelper.isBuffered(this.media, this.media.currentTime) && BufferHelper.isBuffered(this.media, this.media.currentTime + 0.5);
switch (data.details) {
case ErrorDetails.FRAG_LOAD_ERROR:
case ErrorDetails.FRAG_LOAD_TIMEOUT:
case ErrorDetails.KEY_LOAD_ERROR:
case ErrorDetails.KEY_LOAD_TIMEOUT:
if (!data.fatal) {
// keep retrying until the limit will be reached
if ((this.fragLoadError + 1) <= this.config.fragLoadingMaxRetry) {
// exponential backoff capped to config.fragLoadingMaxRetryTimeout
const delay = Math.min(Math.pow(2, this.fragLoadError) * this.config.fragLoadingRetryDelay, this.config.fragLoadingMaxRetryTimeout);
// @ts-ignore - frag is potentially null according to TS here
this.warn(`Fragment ${frag?.sn} of level ${frag?.level} failed to load, retrying in ${delay}ms`);
this.retryDate = self.performance.now() + delay;
// retry loading state
// if loadedmetadata is not set, it means that we are emergency switch down on first frag
// in that case, reset startFragRequested flag
if (!this.loadedmetadata) {
this.startFragRequested = false;
this.nextLoadPosition = this.startPosition;
case ErrorDetails.FRAG_LOAD_ERROR:
case ErrorDetails.FRAG_LOAD_TIMEOUT:
case ErrorDetails.KEY_LOAD_ERROR:
case ErrorDetails.KEY_LOAD_TIMEOUT:
if (!data.fatal) {
// keep retrying until the limit will be reached
if (this.fragLoadError + 1 <= this.config.fragLoadingMaxRetry) {
// exponential backoff capped to config.fragLoadingMaxRetryTimeout
const delay = Math.min(Math.pow(2, this.fragLoadError) * this.config.fragLoadingRetryDelay, this.config.fragLoadingMaxRetryTimeout);
// @ts-ignore - frag is potentially null according to TS here
this.warn(`Fragment ${frag?.sn} of level ${frag?.level} failed to load, retrying in ${delay}ms`);
this.retryDate = self.performance.now() + delay;
// retry loading state
// if loadedmetadata is not set, it means that we are emergency switch down on first frag
// in that case, reset startFragRequested flag
if (!this.loadedmetadata) {
this.startFragRequested = false;
this.nextLoadPosition = this.startPosition;
}
this.fragLoadError++;
this.state = State.FRAG_LOADING_WAITING_RETRY;
} else {
logger.error(`[stream-controller]: ${data.details} reaches max retry, redispatch as fatal ...`);
// switch error to fatal
data.fatal = true;
this.state = State.ERROR;
}
this.fragLoadError++;
this.state = State.FRAG_LOADING_WAITING_RETRY;
} else {
logger.error(`[stream-controller]: ${data.details} reaches max retry, redispatch as fatal ...`);
// switch error to fatal
data.fatal = true;
this.state = State.ERROR;
}
}
break;
case ErrorDetails.LEVEL_LOAD_ERROR:
case ErrorDetails.LEVEL_LOAD_TIMEOUT:
if (this.state !== State.ERROR) {
if (data.fatal) {
// if fatal error, stop processing
this.warn(`${data.details}`);
this.state = State.ERROR;
} else {
// in case of non fatal error while loading level, if level controller is not retrying to load level , switch back to IDLE
if (!data.levelRetry && this.state === State.WAITING_LEVEL) {
break;
case ErrorDetails.LEVEL_LOAD_ERROR:
case ErrorDetails.LEVEL_LOAD_TIMEOUT:
if (this.state !== State.ERROR) {
if (data.fatal) {
// if fatal error, stop processing
this.warn(`${data.details}`);
this.state = State.ERROR;
} else {
// in case of non fatal error while loading level, if level controller is not retrying to load level , switch back to IDLE
if (!data.levelRetry && this.state === State.WAITING_LEVEL) {
this.state = State.IDLE;
}
}
}
break;
case ErrorDetails.BUFFER_FULL_ERROR:
// if in appending state
if (data.parent === 'main' && (this.state === State.PARSING || this.state === State.PARSED)) {
// reduce max buf len if current position is buffered
if (mediaBuffered) {
this._reduceMaxBufferLength(this.config.maxBufferLength);
this.state = State.IDLE;
} else {
// current position is not buffered, but browser is still complaining about buffer full error
// this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708
// in that case flush the whole buffer to recover
this.warn('buffer full error also media.currentTime is not buffered, flush everything');
this.fragCurrent = null;
// flush everything
this.flushMainBuffer(0, Number.POSITIVE_INFINITY);
}
}
}
break;
case ErrorDetails.BUFFER_FULL_ERROR:
// if in appending state
if (data.parent === 'main' && (this.state === State.PARSING || this.state === State.PARSED)) {
// reduce max buf len if current position is buffered
if (mediaBuffered) {
this._reduceMaxBufferLength(this.config.maxBufferLength);
this.state = State.IDLE;
} else {
// current position is not buffered, but browser is still complaining about buffer full error
// this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708
// in that case flush the whole buffer to recover
this.warn('buffer full error also media.currentTime is not buffered, flush everything');
this.fragCurrent = null;
// flush everything
this.flushMainBuffer(0, Number.POSITIVE_INFINITY);
}
}
break;
default:
break;
break;
default:
break;
}
}
_reduceMaxBufferLength (minLength) {
_reduceMaxBufferLength(minLength) {
const config = this.config;
if (config.maxMaxBufferLength >= minLength) {
// reduce max buffer length as it might be too high. we do this to avoid loop flushing ...
@@ -839,7 +829,7 @@ export default class StreamController extends BaseStreamController implements Ne
}
// Checks the health of the buffer and attempts to resolve playback stalls.
private checkBuffer () {
private checkBuffer() {
const { media, gapController } = this;
if (!media || !gapController || !media.readyState) {
// Exit early if we don't have media or if the media hasn't buffered anything yet (readyState 0)
@@ -862,7 +852,7 @@ export default class StreamController extends BaseStreamController implements Ne
this.lastCurrentTime = media.currentTime;
}
onFragLoadEmergencyAborted () {
onFragLoadEmergencyAborted() {
this.state = State.IDLE;
// if loadedmetadata is not set, it means that we are emergency switch down on first frag
// in that case, reset startFragRequested flag
@@ -873,7 +863,7 @@ export default class StreamController extends BaseStreamController implements Ne
this.tick();
}
onBufferFlushed (event: Events.BUFFER_FLUSHED, { type }: BufferFlushedData) {
onBufferFlushed(event: Events.BUFFER_FLUSHED, { type }: BufferFlushedData) {
/* after successful buffer flushing, filter flushed fragments from bufferedFrags
use mediaBuffered instead of media (so that we will check against video.buffered ranges in case of alt audio track)
*/
@@ -887,11 +877,11 @@ export default class StreamController extends BaseStreamController implements Ne
this.state = State.IDLE;
}
onLevelsUpdated (event: Events.LEVELS_UPDATED, data: LevelsUpdatedData) {
onLevelsUpdated(event: Events.LEVELS_UPDATED, data: LevelsUpdatedData) {
this.levels = data.levels;
}
swapAudioCodec () {
swapAudioCodec() {
this.audioCodecSwap = !this.audioCodecSwap;
}
@@ -899,7 +889,7 @@ export default class StreamController extends BaseStreamController implements Ne
* Seeks to the set startPosition if not equal to the mediaElement's current time.
* @private
*/
_seekToStartPos () {
_seekToStartPos() {
const { media } = this;
const currentTime = media.currentTime;
let startPosition = this.startPosition;
@@ -923,7 +913,7 @@ export default class StreamController extends BaseStreamController implements Ne
}
}
_getAudioCodec (currentLevel) {
_getAudioCodec(currentLevel) {
let audioCodec = this.config.defaultAudioCodec || currentLevel.audioCodec;
if (this.audioCodecSwap) {
this.log('Swapping playlist audio codec');
@@ -939,32 +929,31 @@ export default class StreamController extends BaseStreamController implements Ne
return audioCodec;
}
private _loadBitrateTestFrag (frag: Fragment) {
this._doFragLoad(frag)
.then((data) => {
const { hls } = this;
if (!data || hls.nextLoadLevel || this.fragContextChanged(frag)) {
return;
}
this.fragLoadError = 0;
this.state = State.IDLE;
this.startFragRequested = false;
this.bitrateTest = false;
frag.bitrateTest = false;
const stats = frag.stats;
// Bitrate tests fragments are neither parsed nor buffered
stats.parsing.start = stats.parsing.end = stats.buffering.start = stats.buffering.end = self.performance.now();
hls.trigger(Events.FRAG_BUFFERED, {
stats,
frag,
part: null,
id: 'main'
});
this.tick();
private _loadBitrateTestFrag(frag: Fragment) {
this._doFragLoad(frag).then((data) => {
const { hls } = this;
if (!data || hls.nextLoadLevel || this.fragContextChanged(frag)) {
return;
}
this.fragLoadError = 0;
this.state = State.IDLE;
this.startFragRequested = false;
this.bitrateTest = false;
frag.bitrateTest = false;
const stats = frag.stats;
// Bitrate tests fragments are neither parsed nor buffered
stats.parsing.start = stats.parsing.end = stats.buffering.start = stats.buffering.end = self.performance.now();
hls.trigger(Events.FRAG_BUFFERED, {
stats,
frag,
part: null,
id: 'main',
});
this.tick();
});
}
private _handleTransmuxComplete (transmuxResult: TransmuxerResult) {
private _handleTransmuxComplete(transmuxResult: TransmuxerResult) {
const id = 'main';
const { hls } = this;
const { remuxResult, chunkMeta } = transmuxResult;
@@ -1033,7 +1022,7 @@ export default class StreamController extends BaseStreamController implements Ne
const emittedID3: FragParsingMetadataData = {
frag,
id,
samples: id3.samples
samples: id3.samples,
};
hls.trigger(Events.FRAG_PARSING_METADATA, emittedID3);
}
@@ -1041,13 +1030,13 @@ export default class StreamController extends BaseStreamController implements Ne
const emittedText: FragParsingUserdataData = {
frag,
id,
samples: text.samples
samples: text.samples,
};
hls.trigger(Events.FRAG_PARSING_USERDATA, emittedText);
}
}
private _bufferInitSegment (currentLevel: Level, tracks: TrackSet, frag: Fragment, chunkMeta: ChunkMetadata) {
private _bufferInitSegment(currentLevel: Level, tracks: TrackSet, frag: Fragment, chunkMeta: ChunkMetadata) {
if (this.state !== State.PARSING) {
return;
}
@@ -1080,7 +1069,8 @@ export default class StreamController extends BaseStreamController implements Ne
}
}
// HE-AAC is broken on Android, always signal audio codec as AAC even if variant manifest states otherwise
if (ua.indexOf('android') !== -1 && audio.container !== 'audio/mpeg') { // Exclude mpeg audio
if (ua.indexOf('android') !== -1 && audio.container !== 'audio/mpeg') {
// Exclude mpeg audio
audioCodec = 'mp4a.40.2';
this.log(`Android: force audio codec to ${audioCodec}`);
}
@@ -1093,7 +1083,7 @@ export default class StreamController extends BaseStreamController implements Ne
}
this.hls.trigger(Events.BUFFER_CODECS, tracks);
// loop through tracks that are going to be provided to bufferController
Object.keys(tracks).forEach(trackName => {
Object.keys(tracks).forEach((trackName) => {
const track = tracks[trackName];
const initSegment = track.initSegment;
this.log(`Main track:${trackName},container:${track.container},codecs[level/parsed]=[${track.levelCodec}/${track.codec}]`);
@@ -1103,7 +1093,7 @@ export default class StreamController extends BaseStreamController implements Ne
data: initSegment,
frag,
part: null,
chunkMeta
chunkMeta,
});
}
});
@@ -1111,13 +1101,13 @@ export default class StreamController extends BaseStreamController implements Ne
this.tick();
}
private backtrack () {
private backtrack() {
// Causes findFragments to backtrack through fragments to find the keyframe
this.resetTransmuxer();
this.state = State.BACKTRACKING;
}
private checkFragmentChanged () {
private checkFragmentChanged() {
const video = this.media;
let fragPlayingCurrent: Fragment | null = null;
if (video && video.readyState > 1 && video.seeking === false) {
@@ -1142,8 +1132,7 @@ export default class StreamController extends BaseStreamController implements Ne
if (fragPlayingCurrent) {
const fragPlaying = this.fragPlaying;
const fragCurrentLevel = fragPlayingCurrent.level;
if (!fragPlaying || fragPlayingCurrent.sn !== fragPlaying.sn || fragPlaying.level !== fragCurrentLevel ||
fragPlayingCurrent.urlId !== fragPlaying.urlId) {
if (!fragPlaying || fragPlayingCurrent.sn !== fragPlaying.sn || fragPlaying.level !== fragCurrentLevel || fragPlayingCurrent.urlId !== fragPlaying.urlId) {
this.hls.trigger(Events.FRAG_CHANGED, { frag: fragPlayingCurrent });
if (!fragPlaying || fragPlaying.level !== fragCurrentLevel) {
this.hls.trigger(Events.LEVEL_SWITCHED, { level: fragCurrentLevel });
@@ -1154,7 +1143,7 @@ export default class StreamController extends BaseStreamController implements Ne
}
}
get nextLevel () {
get nextLevel() {
const frag = this.nextBufferedFrag;
if (frag) {
return frag.level;
@@ -1163,7 +1152,7 @@ export default class StreamController extends BaseStreamController implements Ne
}
}
get currentLevel () {
get currentLevel() {
const media = this.media;
if (media) {
const fragPlayingCurrent = this.getAppendedFrag(media.currentTime);
@@ -1174,7 +1163,7 @@ export default class StreamController extends BaseStreamController implements Ne
return -1;
}
get nextBufferedFrag () {
get nextBufferedFrag() {
const media = this.media;
if (media) {
// first get end range of current fragment
@@ -1185,7 +1174,7 @@ export default class StreamController extends BaseStreamController implements Ne
}
}
get forceStartLoad () {
get forceStartLoad() {
return this._forceStartLoad;
}
}
+24 -31
View File
@@ -11,20 +11,13 @@ import type { NetworkComponentAPI } from '../types/component-api';
import type Hls from '../hls';
import type LevelDetails from '../loader/level-details';
import type Fragment from '../loader/fragment';
import type {
ErrorData, FragLoadedData,
MediaAttachedData,
SubtitleFragProcessed,
SubtitleTracksUpdatedData,
TrackLoadedData,
TrackSwitchedData
} from '../types/events';
import type { ErrorData, FragLoadedData, MediaAttachedData, SubtitleFragProcessed, SubtitleTracksUpdatedData, TrackLoadedData, TrackSwitchedData } from '../types/events';
const TICK_INTERVAL = 500; // how often to tick in ms
interface TimeRange {
start: number,
end: number
start: number;
end: number;
}
export class SubtitleStreamController extends BaseStreamController implements NetworkComponentAPI {
@@ -33,7 +26,7 @@ export class SubtitleStreamController extends BaseStreamController implements Ne
private currentTrackId: number = -1;
private tracksBuffered: Array<TimeRange[]>;
constructor (hls: Hls, fragmentTracker: FragmentTracker) {
constructor(hls: Hls, fragmentTracker: FragmentTracker) {
super(hls, fragmentTracker, '[subtitle-stream-controller]');
this.config = hls.config;
this.fragCurrent = null;
@@ -47,7 +40,7 @@ export class SubtitleStreamController extends BaseStreamController implements Ne
this._registerListeners();
}
private _registerListeners () {
private _registerListeners() {
const { hls } = this;
hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -58,7 +51,7 @@ export class SubtitleStreamController extends BaseStreamController implements Ne
hls.on(Events.SUBTITLE_FRAG_PROCESSED, this.onSubtitleFragProcessed, this);
}
private _unregisterListeners () {
private _unregisterListeners() {
const { hls } = this;
hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -69,7 +62,7 @@ export class SubtitleStreamController extends BaseStreamController implements Ne
hls.off(Events.SUBTITLE_FRAG_PROCESSED, this.onSubtitleFragProcessed, this);
}
startLoad () {
startLoad() {
this.stopLoad();
this.state = State.IDLE;
@@ -81,13 +74,13 @@ export class SubtitleStreamController extends BaseStreamController implements Ne
}
}
onHandlerDestroyed () {
onHandlerDestroyed() {
this.state = State.STOPPED;
this._unregisterListeners();
super.onHandlerDestroyed();
}
onSubtitleFragProcessed (event: Events.SUBTITLE_FRAG_PROCESSED, data: SubtitleFragProcessed) {
onSubtitleFragProcessed(event: Events.SUBTITLE_FRAG_PROCESSED, data: SubtitleFragProcessed) {
const { frag, success } = data;
this.fragPrevious = frag;
this.state = State.IDLE;
@@ -117,18 +110,18 @@ export class SubtitleStreamController extends BaseStreamController implements Ne
} else {
timeRange = {
start: fragStart,
end: fragEnd
end: fragEnd,
};
buffered.push(timeRange);
}
}
onMediaAttached (event: Events.MEDIA_ATTACHED, { media }: MediaAttachedData) {
onMediaAttached(event: Events.MEDIA_ATTACHED, { media }: MediaAttachedData) {
this.media = media;
this.state = State.IDLE;
}
onMediaDetaching () {
onMediaDetaching() {
if (!this.media) {
return;
}
@@ -143,7 +136,7 @@ export class SubtitleStreamController extends BaseStreamController implements Ne
}
// If something goes wrong, proceed to next frag, if we were processing one.
onError (event: Events.ERROR, data: ErrorData) {
onError(event: Events.ERROR, data: ErrorData) {
const frag = data.frag;
// don't handle error not related to subtitle fragment
if (!frag || frag.type !== 'subtitle') {
@@ -158,17 +151,17 @@ export class SubtitleStreamController extends BaseStreamController implements Ne
}
// Got all new subtitle levels.
onSubtitleTracksUpdated (event: Events.SUBTITLE_TRACKS_UPDATED, { subtitleTracks }: SubtitleTracksUpdatedData) {
onSubtitleTracksUpdated(event: Events.SUBTITLE_TRACKS_UPDATED, { subtitleTracks }: SubtitleTracksUpdatedData) {
logger.log('subtitle levels updated');
this.tracksBuffered = [];
this.levels = subtitleTracks.map(mediaPlaylist => new Level(mediaPlaylist));
this.levels = subtitleTracks.map((mediaPlaylist) => new Level(mediaPlaylist));
this.levels.forEach((level: Level) => {
this.tracksBuffered[level.id] = [];
});
this.mediaBuffer = null;
}
onSubtitleTrackSwitch (event: Events.SUBTITLE_TRACK_SWITCH, data: TrackSwitchedData) {
onSubtitleTrackSwitch(event: Events.SUBTITLE_TRACK_SWITCH, data: TrackSwitchedData) {
this.currentTrackId = data.id;
if (!this.levels.length || this.currentTrackId === -1) {
@@ -187,7 +180,7 @@ export class SubtitleStreamController extends BaseStreamController implements Ne
}
// Got a new set of subtitle fragments.
onSubtitleTrackLoaded (event: Events.SUBTITLE_TRACK_LOADED, data: TrackLoadedData) {
onSubtitleTrackLoaded(event: Events.SUBTITLE_TRACK_LOADED, data: TrackLoadedData) {
const { id, details } = data;
const { currentTrackId, levels } = this;
if (!levels.length || !details) {
@@ -211,7 +204,7 @@ export class SubtitleStreamController extends BaseStreamController implements Ne
this.setInterval(TICK_INTERVAL);
}
_handleFragmentLoadComplete (fragLoadedData: FragLoadedData) {
_handleFragmentLoadComplete(fragLoadedData: FragLoadedData) {
const { frag, payload } = fragLoadedData;
const decryptData = frag.decryptdata;
const hls = this.hls;
@@ -230,14 +223,14 @@ export class SubtitleStreamController extends BaseStreamController implements Ne
payload: decryptedData,
stats: {
tstart: startTime,
tdecrypt: endTime
}
tdecrypt: endTime,
},
});
});
}
}
doTick () {
doTick() {
if (!this.media) {
this.state = State.IDLE;
return;
@@ -288,17 +281,17 @@ export class SubtitleStreamController extends BaseStreamController implements Ne
}
}
protected loadFragment (frag: Fragment, levelDetails: LevelDetails, targetBufferTime: number) {
protected loadFragment(frag: Fragment, levelDetails: LevelDetails, targetBufferTime: number) {
this.fragCurrent = frag;
super.loadFragment(frag, levelDetails, targetBufferTime);
}
stopLoad () {
stopLoad() {
this.fragPrevious = null;
super.stopLoad();
}
get mediaBufferTimeRanges () {
get mediaBufferTimeRanges() {
return this.tracksBuffered[this.currentTrackId] || [];
}
}
+29 -37
View File
@@ -3,12 +3,7 @@ import { clearCurrentCues } from '../utils/texttrack-utils';
import BasePlaylistController from './base-playlist-controller';
import type { HlsUrlParameters } from '../types/level';
import type Hls from '../hls';
import type {
TrackLoadedData,
MediaAttachedData,
SubtitleTracksUpdatedData,
ManifestParsedData
} from '../types/events';
import type { TrackLoadedData, MediaAttachedData, SubtitleTracksUpdatedData, ManifestParsedData } from '../types/events';
import type { MediaPlaylist } from '../types/media-playlist';
import { ErrorData, LevelLoadingData } from '../types/events';
import { PlaylistContextType } from '../types/loader';
@@ -27,17 +22,17 @@ class SubtitleTrackController extends BasePlaylistController {
public subtitleDisplay: boolean = true; // Enable/disable subtitle display rendering
constructor (hls: Hls) {
constructor(hls: Hls) {
super(hls, '[subtitle-track-controller]');
this.registerListeners();
}
public destroy () {
public destroy() {
this.unregisterListeners();
super.destroy();
}
private registerListeners () {
private registerListeners() {
const { hls } = this;
hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -48,7 +43,7 @@ class SubtitleTrackController extends BasePlaylistController {
hls.on(Events.ERROR, this.onError, this);
}
private unregisterListeners () {
private unregisterListeners() {
const { hls } = this;
hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this);
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -60,7 +55,7 @@ class SubtitleTrackController extends BasePlaylistController {
}
// Listen for subtitle track change, then extract the current track ID.
protected onMediaAttached (event: Events.MEDIA_ATTACHED, data: MediaAttachedData): void {
protected onMediaAttached(event: Events.MEDIA_ATTACHED, data: MediaAttachedData): void {
this.media = data.media;
if (!this.media) {
return;
@@ -82,7 +77,7 @@ class SubtitleTrackController extends BasePlaylistController {
}
}
protected onMediaDetaching (): void {
protected onMediaDetaching(): void {
if (!this.media) {
return;
}
@@ -107,7 +102,7 @@ class SubtitleTrackController extends BasePlaylistController {
this.media = null;
}
protected onManifestLoading (): void {
protected onManifestLoading(): void {
this.tracks = [];
this.groupId = null;
this.tracksInGroup = [];
@@ -116,11 +111,11 @@ class SubtitleTrackController extends BasePlaylistController {
}
// Fired whenever a new manifest is loaded.
protected onManifestParsed (event: Events.MANIFEST_PARSED, data: ManifestParsedData): void {
protected onManifestParsed(event: Events.MANIFEST_PARSED, data: ManifestParsedData): void {
this.tracks = data.subtitleTracks;
}
protected onSubtitleTrackLoaded (event: Events.SUBTITLE_TRACK_LOADED, data: TrackLoadedData): void {
protected onSubtitleTrackLoaded(event: Events.SUBTITLE_TRACK_LOADED, data: TrackLoadedData): void {
const { id, details } = data;
const { trackId } = this;
const currentTrack = this.tracksInGroup[trackId];
@@ -140,7 +135,7 @@ class SubtitleTrackController extends BasePlaylistController {
}
}
protected onLevelLoading (event: Events.LEVEL_LOADING, data: LevelLoadingData): void {
protected onLevelLoading(event: Events.LEVEL_LOADING, data: LevelLoadingData): void {
const levelInfo = this.hls.levels[data.level];
if (!levelInfo?.textGroupIds) {
@@ -151,8 +146,7 @@ class SubtitleTrackController extends BasePlaylistController {
if (this.groupId !== textGroupId) {
const lastTrack = this.tracksInGroup ? this.tracksInGroup[this.trackId] : undefined;
const initialTrackId = this.findTrackId(lastTrack?.name) || this.findTrackId();
const subtitleTracks = this.tracks.filter((track): boolean =>
!textGroupId || track.groupId === textGroupId);
const subtitleTracks = this.tracks.filter((track): boolean => !textGroupId || track.groupId === textGroupId);
this.groupId = textGroupId;
this.tracksInGroup = subtitleTracks;
const subtitleTracksUpdated: SubtitleTracksUpdatedData = { subtitleTracks };
@@ -164,7 +158,7 @@ class SubtitleTrackController extends BasePlaylistController {
}
}
private findTrackId (name?: string): number {
private findTrackId(name?: string): number {
const audioTracks = this.tracksInGroup;
for (let i = 0; i < audioTracks.length; i++) {
const track = audioTracks[i];
@@ -177,37 +171,35 @@ class SubtitleTrackController extends BasePlaylistController {
return -1;
}
protected onError (event: Events.ERROR, data: ErrorData): void {
protected onError(event: Events.ERROR, data: ErrorData): void {
super.onError(event, data);
if (data.fatal || !data.context) {
return;
}
if (data.context.type === PlaylistContextType.SUBTITLE_TRACK &&
data.context.id === this.trackId &&
data.context.groupId === this.groupId) {
if (data.context.type === PlaylistContextType.SUBTITLE_TRACK && data.context.id === this.trackId && data.context.groupId === this.groupId) {
this.retryLoadingOrFail(data);
}
}
/** get alternate subtitle tracks list from playlist **/
get subtitleTracks (): MediaPlaylist[] {
get subtitleTracks(): MediaPlaylist[] {
return this.tracksInGroup;
}
/** get index of the selected subtitle track (index in subtitle track lists) **/
get subtitleTrack (): number {
get subtitleTrack(): number {
return this.trackId;
}
/** select a subtitle track, based on its index in subtitle track lists**/
set subtitleTrack (newId: number) {
set subtitleTrack(newId: number) {
this.selectDefaultTrack = false;
const lastTrack = this.tracksInGroup ? this.tracksInGroup[this.trackId] : undefined;
this.setSubtitleTrack(newId, lastTrack);
}
protected loadPlaylist (hlsUrlParameters?: HlsUrlParameters): void {
protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void {
const currentTrack = this.tracksInGroup[this.trackId];
if (this.shouldLoadTrack(currentTrack)) {
const id = currentTrack.id;
@@ -225,7 +217,7 @@ class SubtitleTrackController extends BasePlaylistController {
url,
id,
groupId,
deliveryDirectives: hlsUrlParameters || null
deliveryDirectives: hlsUrlParameters || null,
});
}
}
@@ -235,16 +227,16 @@ class SubtitleTrackController extends BasePlaylistController {
* This operates on the DOM textTracks.
* A value of -1 will disable all subtitle tracks.
*/
private toggleTrackModes (newId: number): void {
private toggleTrackModes(newId: number): void {
const { media, subtitleDisplay, trackId } = this;
if (!media) {
return;
}
const textTracks = filterSubtitleTracks(media.textTracks);
const groupTracks = textTracks.filter(track => (track as any).groupId === this.groupId);
const groupTracks = textTracks.filter((track) => (track as any).groupId === this.groupId);
if (newId === -1) {
[].slice.call(textTracks).forEach(track => {
[].slice.call(textTracks).forEach((track) => {
track.mode = 'disabled';
});
} else {
@@ -261,10 +253,10 @@ class SubtitleTrackController extends BasePlaylistController {
}
/**
* This method is responsible for validating the subtitle index and periodically reloading if live.
* Dispatches the SUBTITLE_TRACK_SWITCH event, which instructs the subtitle-stream-controller to load the selected track.
*/
private setSubtitleTrack (newId: number, lastTrack: MediaPlaylist | undefined): void {
* This method is responsible for validating the subtitle index and periodically reloading if live.
* Dispatches the SUBTITLE_TRACK_SWITCH event, which instructs the subtitle-stream-controller to load the selected track.
*/
private setSubtitleTrack(newId: number, lastTrack: MediaPlaylist | undefined): void {
const tracks = this.tracksInGroup;
// setting this.subtitleTrack will trigger internal logic
@@ -302,7 +294,7 @@ class SubtitleTrackController extends BasePlaylistController {
}
}
private onTextTracksChanged (): void {
private onTextTracksChanged(): void {
// Media is undefined when switching streams via loadSource()
if (!this.media || !this.hls.config.renderTextTracksNatively) {
return;
@@ -325,7 +317,7 @@ class SubtitleTrackController extends BasePlaylistController {
}
}
function filterSubtitleTracks (textTrackList: TextTrackList): TextTrack[] {
function filterSubtitleTracks(textTrackList: TextTrackList): TextTrack[] {
const tracks: TextTrack[] = [];
for (let i = 0; i < textTrackList.length; i++) {
const track = textTrackList[i];
+122 -106
View File
@@ -6,15 +6,7 @@ import { logger } from '../utils/logger';
import { sendAddTrackEvent, clearCurrentCues } from '../utils/texttrack-utils';
import { parseIMSC1, IMSC1_CODEC } from '../utils/imsc1-ttml-parser';
import Fragment from '../loader/fragment';
import {
FragParsingUserdataData,
FragLoadedData,
FragDecryptedData,
MediaAttachingData,
ManifestLoadedData,
InitPTSFoundData,
SubtitleTracksUpdatedData
} from '../types/events';
import { FragParsingUserdataData, FragLoadedData, FragDecryptedData, MediaAttachingData, ManifestLoadedData, InitPTSFoundData, SubtitleTracksUpdatedData } from '../types/events';
import type Hls from '../hls';
import type { ComponentAPI } from '../types/component-api';
import type { HlsConfig } from '../config';
@@ -23,18 +15,18 @@ import type { MediaPlaylist } from '../types/media-playlist';
import type { VTTCCs } from '../types/vtt';
type TrackProperties = {
label: string,
languageCode: string,
media?: MediaPlaylist
label: string;
languageCode: string;
media?: MediaPlaylist;
};
type NonNativeCaptionsTrack = {
_id?: string,
label: string,
kind: string,
default: boolean,
closedCaptions?: MediaPlaylist,
subtitleTrack?: MediaPlaylist
_id?: string;
label: string;
kind: string;
default: boolean;
closedCaptions?: MediaPlaylist;
subtitleTrack?: MediaPlaylist;
};
export class TimelineController implements ComponentAPI {
@@ -56,13 +48,13 @@ export class TimelineController implements ComponentAPI {
private prevCC: number = -1;
private vttCCs: VTTCCs = newVTTCCs();
private captionsProperties: {
textTrack1: TrackProperties
textTrack2: TrackProperties
textTrack3: TrackProperties
textTrack4: TrackProperties
textTrack1: TrackProperties;
textTrack2: TrackProperties;
textTrack3: TrackProperties;
textTrack4: TrackProperties;
};
constructor (hls: Hls) {
constructor(hls: Hls) {
this.hls = hls;
this.config = hls.config;
this.Cues = hls.config.cueHandler;
@@ -70,20 +62,20 @@ export class TimelineController implements ComponentAPI {
this.captionsProperties = {
textTrack1: {
label: this.config.captionsTextTrack1Label,
languageCode: this.config.captionsTextTrack1LanguageCode
languageCode: this.config.captionsTextTrack1LanguageCode,
},
textTrack2: {
label: this.config.captionsTextTrack2Label,
languageCode: this.config.captionsTextTrack2LanguageCode
languageCode: this.config.captionsTextTrack2LanguageCode,
},
textTrack3: {
label: this.config.captionsTextTrack3Label,
languageCode: this.config.captionsTextTrack3LanguageCode
languageCode: this.config.captionsTextTrack3LanguageCode,
},
textTrack4: {
label: this.config.captionsTextTrack4Label,
languageCode: this.config.captionsTextTrack4LanguageCode
}
languageCode: this.config.captionsTextTrack4LanguageCode,
},
};
if (this.config.enableCEA708Captions) {
@@ -98,7 +90,7 @@ export class TimelineController implements ComponentAPI {
this._registerListeners();
}
private _registerListeners (): void {
private _registerListeners(): void {
const { hls } = this;
hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -112,7 +104,7 @@ export class TimelineController implements ComponentAPI {
hls.on(Events.SUBTITLE_TRACKS_CLEARED, this.onSubtitleTracksCleared, this);
}
private _unregisterListeners (): void {
private _unregisterListeners(): void {
const { hls } = this;
hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
@@ -126,17 +118,17 @@ export class TimelineController implements ComponentAPI {
hls.off(Events.SUBTITLE_TRACKS_CLEARED, this.onSubtitleTracksCleared, this);
}
addCues (trackName: string, startTime: number, endTime: number, screen: CaptionScreen, cueRanges: Array<[number, number]>) {
addCues(trackName: string, startTime: number, endTime: number, screen: CaptionScreen, cueRanges: Array<[number, number]>) {
// skip cues which overlap more than 50% with previously parsed time ranges
let merged = false;
for (let i = cueRanges.length; i--;) {
for (let i = cueRanges.length; i--; ) {
const cueRange = cueRanges[i];
const overlap = intersection(cueRange[0], cueRange[1], startTime, endTime);
if (overlap >= 0) {
cueRange[0] = Math.min(cueRange[0], startTime);
cueRange[1] = Math.max(cueRange[1], endTime);
merged = true;
if ((overlap / (endTime - startTime)) > 0.5) {
if (overlap / (endTime - startTime) > 0.5) {
return;
}
}
@@ -154,7 +146,7 @@ export class TimelineController implements ComponentAPI {
}
// Triggered when an initial PTS is found; used for synchronisation of WebVTT.
onInitPtsFound (event: Events.INIT_PTS_FOUND, { frag, id, initPTS, timescale }: InitPTSFoundData) {
onInitPtsFound(event: Events.INIT_PTS_FOUND, { frag, id, initPTS, timescale }: InitPTSFoundData) {
const { unparsedVttFrags } = this;
if (id === 'main') {
this.initPTS[frag.cc] = initPTS;
@@ -165,13 +157,13 @@ export class TimelineController implements ComponentAPI {
// Parse any unparsed fragments upon receiving the initial PTS.
if (unparsedVttFrags.length) {
this.unparsedVttFrags = [];
unparsedVttFrags.forEach(frag => {
unparsedVttFrags.forEach((frag) => {
this.onFragLoaded(Events.FRAG_LOADED, frag as FragLoadedData);
});
}
}
getExistingTrack (trackName: string): TextTrack | null {
getExistingTrack(trackName: string): TextTrack | null {
const { media } = this;
if (media) {
for (let i = 0; i < media.textTracks.length; i++) {
@@ -184,7 +176,7 @@ export class TimelineController implements ComponentAPI {
return null;
}
createCaptionsTrack (trackName: string) {
createCaptionsTrack(trackName: string) {
if (this.config.renderTextTracksNatively) {
this.createNativeTrack(trackName);
} else {
@@ -192,7 +184,7 @@ export class TimelineController implements ComponentAPI {
}
}
createNativeTrack (trackName: string) {
createNativeTrack(trackName: string) {
if (this.captionsTracks[trackName]) {
return;
}
@@ -214,7 +206,7 @@ export class TimelineController implements ComponentAPI {
}
}
createNonNativeTrack (trackName: string) {
createNonNativeTrack(trackName: string) {
if (this.nonNativeCaptionsTracks[trackName]) {
return;
}
@@ -229,13 +221,13 @@ export class TimelineController implements ComponentAPI {
label,
kind: 'captions',
default: trackProperties.media ? !!trackProperties.media.default : false,
closedCaptions: trackProperties.media
closedCaptions: trackProperties.media,
};
this.nonNativeCaptionsTracks[trackName] = track;
this.hls.trigger(Events.NON_NATIVE_TEXT_TRACKS_FOUND, { tracks: [track] });
}
createTextTrack (kind: TextTrackKind, label: string, lang?: string): TextTrack | undefined {
createTextTrack(kind: TextTrackKind, label: string, lang?: string): TextTrack | undefined {
const media = this.media;
if (!media) {
return;
@@ -243,25 +235,25 @@ export class TimelineController implements ComponentAPI {
return media.addTextTrack(kind, label, lang);
}
destroy () {
destroy() {
this._unregisterListeners();
}
onMediaAttaching (event: Events.MEDIA_ATTACHING, data: MediaAttachingData) {
onMediaAttaching(event: Events.MEDIA_ATTACHING, data: MediaAttachingData) {
this.media = data.media;
this._cleanTracks();
}
onMediaDetaching () {
onMediaDetaching() {
const { captionsTracks } = this;
Object.keys(captionsTracks).forEach(trackName => {
Object.keys(captionsTracks).forEach((trackName) => {
clearCurrentCues(captionsTracks[trackName]);
delete captionsTracks[trackName];
});
this.nonNativeCaptionsTracks = {};
}
onManifestLoading () {
onManifestLoading() {
this.lastSn = -1; // Detect discontinuity in fragment parsing
this.prevCC = -1;
this.vttCCs = newVTTCCs(); // Detect discontinuity in subtitle manifests
@@ -279,7 +271,7 @@ export class TimelineController implements ComponentAPI {
}
}
_cleanTracks () {
_cleanTracks() {
// clear outdated subtitles
const { media } = this;
if (!media) {
@@ -293,7 +285,7 @@ export class TimelineController implements ComponentAPI {
}
}
onSubtitleTracksUpdated (event: Events.SUBTITLE_TRACKS_UPDATED, data: SubtitleTracksUpdatedData) {
onSubtitleTracksUpdated(event: Events.SUBTITLE_TRACKS_UPDATED, data: SubtitleTracksUpdatedData) {
this.textTracks = [];
const tracks: Array<MediaPlaylist> = data.subtitleTracks || [];
const hasIMSC1 = tracks.some((track) => track.textCodec === IMSC1_CODEC);
@@ -339,7 +331,7 @@ export class TimelineController implements ComponentAPI {
label: track.name,
kind: track.type.toLowerCase(),
default: track.default,
subtitleTrack: track
subtitleTrack: track,
};
});
this.hls.trigger(Events.NON_NATIVE_TEXT_TRACKS_FOUND, { tracks: tracksList });
@@ -347,9 +339,9 @@ export class TimelineController implements ComponentAPI {
}
}
onManifestLoaded (event: Events.MANIFEST_LOADED, data: ManifestLoadedData) {
onManifestLoaded(event: Events.MANIFEST_LOADED, data: ManifestLoadedData) {
if (this.config.enableCEA708Captions && data.captions) {
data.captions.forEach(captionsTrack => {
data.captions.forEach((captionsTrack) => {
const instreamIdMatch = /(?:CC|SERVICE)([1-4])/.exec(captionsTrack.instreamId as string);
if (!instreamIdMatch) {
return;
@@ -360,7 +352,8 @@ export class TimelineController implements ComponentAPI {
return;
}
trackProperties.label = captionsTrack.name;
if (captionsTrack.lang) { // optional attribute
if (captionsTrack.lang) {
// optional attribute
trackProperties.languageCode = captionsTrack.lang;
}
trackProperties.media = captionsTrack;
@@ -368,7 +361,7 @@ export class TimelineController implements ComponentAPI {
}
}
onFragLoaded (event: Events.FRAG_LOADED, data: FragLoadedData) {
onFragLoaded(event: Events.FRAG_LOADED, data: FragLoadedData) {
const { frag, payload } = data;
const { cea608Parser1, cea608Parser2, initPTS, lastSn, unparsedVttFrags } = this;
if (frag.type === 'main') {
@@ -381,7 +374,8 @@ export class TimelineController implements ComponentAPI {
}
}
this.lastSn = sn as number;
} else if (frag.type === 'subtitle') { // If fragment is subtitle type, parse as WebVTT.
} else if (frag.type === 'subtitle') {
// If fragment is subtitle type, parse as WebVTT.
if (payload.byteLength) {
// We need an initial synchronisation PTS. Store fragments as long as none has arrived.
if (!Number.isFinite(initPTS[frag.cc])) {
@@ -395,7 +389,7 @@ export class TimelineController implements ComponentAPI {
const decryptData = frag.decryptdata;
// If the subtitles are not encrypted, parse VTTs now. Otherwise, we need to wait.
if ((decryptData == null) || (decryptData.key == null) || (decryptData.method !== 'AES-128')) {
if (decryptData == null || decryptData.key == null || decryptData.method !== 'AES-128') {
const trackPlaylistMedia = this.tracks[frag.level];
const vttCCs = this.vttCCs;
if (!vttCCs[frag.cc]) {
@@ -415,45 +409,65 @@ export class TimelineController implements ComponentAPI {
}
}
private _parseIMSC1 (frag: Fragment, payload: ArrayBuffer) {
private _parseIMSC1(frag: Fragment, payload: ArrayBuffer) {
const hls = this.hls;
parseIMSC1(payload, this.initPTS[frag.cc], this.timescale[frag.cc], (cues) => {
this._appendCues(cues, frag.level);
hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { success: true, frag: frag });
}, (error) => {
logger.log(`Failed to parse IMSC1: ${error}`);
hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { success: false, frag: frag, error });
});
parseIMSC1(
payload,
this.initPTS[frag.cc],
this.timescale[frag.cc],
(cues) => {
this._appendCues(cues, frag.level);
hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { success: true, frag: frag });
},
(error) => {
logger.log(`Failed to parse IMSC1: ${error}`);
hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { success: false, frag: frag, error });
}
);
}
private _parseVTTs (frag: Fragment, payload: ArrayBuffer, vttCCs: any) {
private _parseVTTs(frag: Fragment, payload: ArrayBuffer, vttCCs: any) {
const hls = this.hls;
// Parse the WebVTT file contents.
parseWebVTT(payload, this.initPTS[frag.cc], this.timescale[frag.cc], vttCCs, frag.cc, (cues) => {
this._appendCues(cues, frag.level);
hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { success: true, frag: frag });
}, (error) => {
this._fallbackToIMSC1(frag, payload);
// Something went wrong while parsing. Trigger event with success false.
logger.log(`Failed to parse VTT cue: ${error}`);
hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { success: false, frag: frag, error });
});
parseWebVTT(
payload,
this.initPTS[frag.cc],
this.timescale[frag.cc],
vttCCs,
frag.cc,
(cues) => {
this._appendCues(cues, frag.level);
hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { success: true, frag: frag });
},
(error) => {
this._fallbackToIMSC1(frag, payload);
// Something went wrong while parsing. Trigger event with success false.
logger.log(`Failed to parse VTT cue: ${error}`);
hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { success: false, frag: frag, error });
}
);
}
private _fallbackToIMSC1 (frag: Fragment, payload: ArrayBuffer) {
private _fallbackToIMSC1(frag: Fragment, payload: ArrayBuffer) {
// If textCodec is unknown, try parsing as IMSC1. Set textCodec based on the result
const trackPlaylistMedia = this.tracks[frag.level];
if (!trackPlaylistMedia.textCodec) {
parseIMSC1(payload, this.initPTS[frag.cc], this.timescale[frag.cc], () => {
trackPlaylistMedia.textCodec = IMSC1_CODEC;
this._parseIMSC1(frag, payload);
}, () => {
trackPlaylistMedia.textCodec = 'wvtt';
});
parseIMSC1(
payload,
this.initPTS[frag.cc],
this.timescale[frag.cc],
() => {
trackPlaylistMedia.textCodec = IMSC1_CODEC;
this._parseIMSC1(frag, payload);
},
() => {
trackPlaylistMedia.textCodec = 'wvtt';
}
);
}
}
private _appendCues (cues, fragLevel) {
private _appendCues(cues, fragLevel) {
const hls = this.hls;
if (this.config.renderTextTracksNatively) {
const textTrack = this.textTracks[fragLevel];
@@ -467,19 +481,21 @@ export class TimelineController implements ComponentAPI {
// Sometimes there are cue overlaps on segmented vtts so the same
// cue can appear more than once in different vtt files.
// This avoid showing duplicated cues with same timecode and text.
cues.filter(cue => !textTrack.cues!.getCueById(cue.id)).forEach(cue => {
try {
textTrack.addCue(cue);
if (!textTrack.cues!.getCueById(cue.id)) {
throw new Error(`addCue is failed for: ${cue}`);
cues
.filter((cue) => !textTrack.cues!.getCueById(cue.id))
.forEach((cue) => {
try {
textTrack.addCue(cue);
if (!textTrack.cues!.getCueById(cue.id)) {
throw new Error(`addCue is failed for: ${cue}`);
}
} catch (err) {
logger.debug(`Failed occurred on adding cues: ${err}`);
const textTrackCue = new (self.TextTrackCue as any)(cue.startTime, cue.endTime, cue.text);
textTrackCue.id = cue.id;
textTrack.addCue(textTrackCue);
}
} catch (err) {
logger.debug(`Failed occurred on adding cues: ${err}`);
const textTrackCue = new (self.TextTrackCue as any)(cue.startTime, cue.endTime, cue.text);
textTrackCue.id = cue.id;
textTrack.addCue(textTrackCue);
}
});
});
} else {
const currentTrack = this.tracks[fragLevel];
const track = currentTrack.default ? 'default' : 'subtitles' + fragLevel;
@@ -487,23 +503,23 @@ export class TimelineController implements ComponentAPI {
}
}
onFragDecrypted (event: Events.FRAG_DECRYPTED, data: FragDecryptedData) {
onFragDecrypted(event: Events.FRAG_DECRYPTED, data: FragDecryptedData) {
const { frag } = data;
if (frag.type === 'subtitle') {
if (!Number.isFinite(this.initPTS[frag.cc])) {
this.unparsedVttFrags.push(data as unknown as FragLoadedData);
this.unparsedVttFrags.push((data as unknown) as FragLoadedData);
return;
}
this.onFragLoaded(Events.FRAG_LOADED, data as unknown as FragLoadedData);
this.onFragLoaded(Events.FRAG_LOADED, (data as unknown) as FragLoadedData);
}
}
onSubtitleTracksCleared () {
onSubtitleTracksCleared() {
this.tracks = [];
this.captionsTracks = {};
}
onFragParsingUserdata (event: Events.FRAG_PARSING_USERDATA, data: FragParsingUserdataData) {
onFragParsingUserdata(event: Events.FRAG_PARSING_USERDATA, data: FragParsingUserdataData) {
const { cea608Parser1, cea608Parser2 } = this;
if (!this.enabled || !(cea608Parser1 && cea608Parser2)) {
return;
@@ -521,15 +537,15 @@ export class TimelineController implements ComponentAPI {
}
}
extractCea608Data (byteArray: Uint8Array): number[][] {
extractCea608Data(byteArray: Uint8Array): number[][] {
const count = byteArray[0] & 31;
let position = 2;
const actualCCBytes: number[][] = [[], []];
for (let j = 0; j < count; j++) {
const tmpByte = byteArray[position++];
const ccbyte1 = 0x7F & byteArray[position++];
const ccbyte2 = 0x7F & byteArray[position++];
const ccbyte1 = 0x7f & byteArray[position++];
const ccbyte2 = 0x7f & byteArray[position++];
const ccValid = (4 & tmpByte) !== 0;
const ccType = 3 & tmpByte;
@@ -548,22 +564,22 @@ export class TimelineController implements ComponentAPI {
}
}
function canReuseVttTextTrack (inUseTrack, manifestTrack): boolean {
function canReuseVttTextTrack(inUseTrack, manifestTrack): boolean {
return inUseTrack && inUseTrack.label === manifestTrack.name && !(inUseTrack.textTrack1 || inUseTrack.textTrack2);
}
function intersection (x1: number, x2: number, y1: number, y2: number): number {
function intersection(x1: number, x2: number, y1: number, y2: number): number {
return Math.min(x2, y2) - Math.max(x1, y1);
}
function newVTTCCs (): VTTCCs {
function newVTTCCs(): VTTCCs {
return {
ccOffset: 0,
presentationOffset: 0,
0: {
start: 0,
prevCC: -1,
new: false
}
new: false,
},
};
}
+2 -2
View File
@@ -2,12 +2,12 @@ export default class AESCrypto {
private subtle: SubtleCrypto;
private aesIV: ArrayBuffer;
constructor (subtle: SubtleCrypto, iv: ArrayBuffer) {
constructor(subtle: SubtleCrypto, iv: ArrayBuffer) {
this.subtle = subtle;
this.aesIV = iv;
}
decrypt (data: ArrayBuffer, key: CryptoKey) {
decrypt(data: ArrayBuffer, key: CryptoKey) {
return this.subtle.decrypt({ name: 'AES-CBC', iv: this.aesIV }, key, data);
}
}
+18 -18
View File
@@ -1,9 +1,9 @@
import { sliceUint8 } from '../utils/typed-array';
// PKCS7
export function removePadding (array: Uint8Array): Uint8Array {
export function removePadding(array: Uint8Array): Uint8Array {
const outputBytes = array.byteLength;
const paddingBytes = outputBytes && (new DataView(array.buffer)).getUint8(outputBytes - 1);
const paddingBytes = outputBytes && new DataView(array.buffer).getUint8(outputBytes - 1);
if (paddingBytes) {
return sliceUint8(array, 0, outputBytes - paddingBytes);
}
@@ -14,7 +14,7 @@ export default class AESDecryptor {
private rcon: Array<number> = [0x0, 0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
private subMix: Array<Uint32Array> = [new Uint32Array(256), new Uint32Array(256), new Uint32Array(256), new Uint32Array(256)];
private invSubMix: Array<Uint32Array> = [new Uint32Array(256), new Uint32Array(256), new Uint32Array(256), new Uint32Array(256)];
private sBox: Uint32Array= new Uint32Array(256);
private sBox: Uint32Array = new Uint32Array(256);
private invSBox: Uint32Array = new Uint32Array(256);
private key: Uint32Array = new Uint32Array(0);
@@ -23,12 +23,12 @@ export default class AESDecryptor {
private keySchedule!: Uint32Array;
private invKeySchedule!: Uint32Array;
constructor () {
constructor() {
this.initTable();
}
// Using view.getUint32() also swaps the byte order.
uint8ArrayToUint32Array_ (arrayBuffer) {
uint8ArrayToUint32Array_(arrayBuffer) {
const view = new DataView(arrayBuffer);
const newArray = new Uint32Array(4);
for (let i = 0; i < 4; i++) {
@@ -38,7 +38,7 @@ export default class AESDecryptor {
return newArray;
}
initTable () {
initTable() {
const sBox = this.sBox;
const invSBox = this.invSBox;
const subMix = this.subMix;
@@ -99,14 +99,14 @@ export default class AESDecryptor {
}
}
expandKey (keyBuffer: ArrayBuffer) {
expandKey(keyBuffer: ArrayBuffer) {
// convert keyBuffer to Uint32Array
const key = this.uint8ArrayToUint32Array_(keyBuffer);
let sameKey = true;
let offset = 0;
while (offset < key.length && sameKey) {
sameKey = (key[offset] === this.key[offset]);
sameKey = key[offset] === this.key[offset];
offset++;
}
@@ -115,18 +115,18 @@ export default class AESDecryptor {
}
this.key = key;
const keySize = this.keySize = key.length;
const keySize = (this.keySize = key.length);
if (keySize !== 4 && keySize !== 6 && keySize !== 8) {
throw new Error('Invalid aes key size=' + keySize);
}
const ksRows = this.ksRows = (keySize + 6 + 1) * 4;
const ksRows = (this.ksRows = (keySize + 6 + 1) * 4);
let ksRow;
let invKsRow;
const keySchedule = this.keySchedule = new Uint32Array(ksRows);
const invKeySchedule = this.invKeySchedule = new Uint32Array(ksRows);
const keySchedule = (this.keySchedule = new Uint32Array(ksRows));
const invKeySchedule = (this.invKeySchedule = new Uint32Array(ksRows));
const sbox = this.sBox;
const rcon = this.rcon;
@@ -182,11 +182,11 @@ export default class AESDecryptor {
}
// Adding this as a method greatly improves performance.
networkToHostOrderSwap (word) {
networkToHostOrderSwap(word) {
return (word << 24) | ((word & 0xff00) << 8) | ((word & 0xff0000) >> 8) | (word >>> 24);
}
decrypt (inputArrayBuffer: ArrayBuffer, offset: number, aesIV: ArrayBuffer) {
decrypt(inputArrayBuffer: ArrayBuffer, offset: number, aesIV: ArrayBuffer) {
const nRounds = this.keySize + 6;
const invKeySchedule = this.invKeySchedule;
const invSBOX = this.invSBox;
@@ -242,10 +242,10 @@ export default class AESDecryptor {
}
// Shift rows, sub bytes, add round key
t0 = ((invSBOX[s0 >>> 24] << 24) ^ (invSBOX[(s1 >> 16) & 0xff] << 16) ^ (invSBOX[(s2 >> 8) & 0xff] << 8) ^ invSBOX[s3 & 0xff]) ^ invKeySchedule[ksRow];
t1 = ((invSBOX[s1 >>> 24] << 24) ^ (invSBOX[(s2 >> 16) & 0xff] << 16) ^ (invSBOX[(s3 >> 8) & 0xff] << 8) ^ invSBOX[s0 & 0xff]) ^ invKeySchedule[ksRow + 1];
t2 = ((invSBOX[s2 >>> 24] << 24) ^ (invSBOX[(s3 >> 16) & 0xff] << 16) ^ (invSBOX[(s0 >> 8) & 0xff] << 8) ^ invSBOX[s1 & 0xff]) ^ invKeySchedule[ksRow + 2];
t3 = ((invSBOX[s3 >>> 24] << 24) ^ (invSBOX[(s0 >> 16) & 0xff] << 16) ^ (invSBOX[(s1 >> 8) & 0xff] << 8) ^ invSBOX[s2 & 0xff]) ^ invKeySchedule[ksRow + 3];
t0 = (invSBOX[s0 >>> 24] << 24) ^ (invSBOX[(s1 >> 16) & 0xff] << 16) ^ (invSBOX[(s2 >> 8) & 0xff] << 8) ^ invSBOX[s3 & 0xff] ^ invKeySchedule[ksRow];
t1 = (invSBOX[s1 >>> 24] << 24) ^ (invSBOX[(s2 >> 16) & 0xff] << 16) ^ (invSBOX[(s3 >> 8) & 0xff] << 8) ^ invSBOX[s0 & 0xff] ^ invKeySchedule[ksRow + 1];
t2 = (invSBOX[s2 >>> 24] << 24) ^ (invSBOX[(s3 >> 16) & 0xff] << 16) ^ (invSBOX[(s0 >> 8) & 0xff] << 8) ^ invSBOX[s1 & 0xff] ^ invKeySchedule[ksRow + 2];
t3 = (invSBOX[s3 >>> 24] << 24) ^ (invSBOX[(s0 >> 16) & 0xff] << 16) ^ (invSBOX[(s1 >> 8) & 0xff] << 8) ^ invSBOX[s2 & 0xff] ^ invKeySchedule[ksRow + 3];
ksRow = ksRow + 3;
// Write
+15 -12
View File
@@ -22,7 +22,7 @@ export default class Decrypter {
private currentIV: ArrayBuffer | null = null;
private currentResult: ArrayBuffer | null = null;
constructor (observer: HlsEventEmitter, config: HlsConfig, { removePKCS7Padding = true } = {}) {
constructor(observer: HlsEventEmitter, config: HlsConfig, { removePKCS7Padding = true } = {}) {
this.observer = observer;
this.config = config;
this.removePKCS7Padding = removePKCS7Padding;
@@ -31,19 +31,21 @@ export default class Decrypter {
try {
const browserCrypto = self.crypto;
if (browserCrypto) {
this.subtle = browserCrypto.subtle || (browserCrypto as any).webkitSubtle as SubtleCrypto;
this.subtle = browserCrypto.subtle || ((browserCrypto as any).webkitSubtle as SubtleCrypto);
} else {
this.config.enableSoftwareAES = true;
}
} catch (e) { /* no-op */ }
} catch (e) {
/* no-op */
}
}
}
isSync () {
isSync() {
return this.config.enableSoftwareAES;
}
flush (): Uint8Array | void {
flush(): Uint8Array | void {
const { currentResult } = this;
if (!currentResult) {
this.reset();
@@ -57,7 +59,7 @@ export default class Decrypter {
return data;
}
reset () {
reset() {
this.currentResult = null;
this.currentIV = null;
this.remainderData = null;
@@ -66,7 +68,7 @@ export default class Decrypter {
}
}
public softwareDecrypt (data: Uint8Array, key: ArrayBuffer, iv: ArrayBuffer): ArrayBuffer | null {
public softwareDecrypt(data: Uint8Array, key: ArrayBuffer, iv: ArrayBuffer): ArrayBuffer | null {
const { currentIV, currentResult, remainderData } = this;
this.logOnce('JS AES decrypt');
// The output is staggered during progressive parsing - the current result is cached, and emitted on the next call
@@ -106,13 +108,14 @@ export default class Decrypter {
return result;
}
public webCryptoDecrypt (data: Uint8Array, key: ArrayBuffer, iv: ArrayBuffer): Promise<ArrayBuffer> {
public webCryptoDecrypt(data: Uint8Array, key: ArrayBuffer, iv: ArrayBuffer): Promise<ArrayBuffer> {
const subtle = this.subtle;
if (this.key !== key || !this.fastAesKey) {
this.key = key;
this.fastAesKey = new FastAESKey(subtle, key);
}
return this.fastAesKey.expandKey()
return this.fastAesKey
.expandKey()
.then((aesKey) => {
// decrypt using web crypto
if (!subtle) {
@@ -127,14 +130,14 @@ export default class Decrypter {
});
}
private onWebCryptoError (err, data, key, iv): ArrayBuffer | null {
private onWebCryptoError(err, data, key, iv): ArrayBuffer | null {
logger.warn('[decrypter.ts]: WebCrypto Error, disable WebCrypto API:', err);
this.config.enableSoftwareAES = true;
this.logEnabled = true;
return this.softwareDecrypt(data, key, iv);
}
private getValidChunk (data: Uint8Array) : Uint8Array {
private getValidChunk(data: Uint8Array): Uint8Array {
let currentChunk = data;
const splitPoint = data.length - (data.length % CHUNK_SIZE);
if (splitPoint !== data.length) {
@@ -144,7 +147,7 @@ export default class Decrypter {
return currentChunk;
}
private logOnce (msg: string) {
private logOnce(msg: string) {
if (!this.logEnabled) {
return;
}
+2 -2
View File
@@ -2,12 +2,12 @@ export default class FastAESKey {
private subtle: any;
private key: ArrayBuffer;
constructor (subtle, key) {
constructor(subtle, key) {
this.subtle = subtle;
this.key = key;
}
expandKey () {
expandKey() {
return this.subtle.importKey('raw', this.key, { name: 'AES-CBC' }, false, ['encrypt', 'decrypt']);
}
}
+6 -6
View File
@@ -13,13 +13,13 @@ class AACDemuxer extends BaseAudioDemuxer {
private readonly config: HlsConfig;
static readonly minProbeByteLength: number = 9;
constructor (observer, config) {
constructor(observer, config) {
super();
this.observer = observer;
this.config = config;
}
resetInitSegment (audioCodec, videoCodec, duration) {
resetInitSegment(audioCodec, videoCodec, duration) {
super.resetInitSegment(audioCodec, videoCodec, duration);
this._audioTrack = {
container: 'audio/adts',
@@ -32,12 +32,12 @@ class AACDemuxer extends BaseAudioDemuxer {
manifestCodec: audioCodec,
duration: duration,
inputTimeScale: 90000,
dropped: 0
dropped: 0,
};
}
// Source for probe info - https://wiki.multimedia.cx/index.php?title=ADTS
static probe (data): boolean {
static probe(data): boolean {
if (!data) {
return false;
}
@@ -58,11 +58,11 @@ class AACDemuxer extends BaseAudioDemuxer {
return false;
}
canParse (data, offset) {
canParse(data, offset) {
return ADTS.canParse(data, offset);
}
appendFrame (track, data, offset) {
appendFrame(track, data, offset) {
ADTS.initTrackConfig(track, this.observer, data, offset, track.manifestCodec);
return ADTS.appendFrame(track, data, offset, this.initPTS as number, this.frameIndex);
}
+49 -56
View File
@@ -9,45 +9,45 @@ import { Events } from '../events';
import type { DemuxedAudioTrack, AppendedAudioFrame } from '../types/demuxer';
type AudioConfig = {
config: number[],
samplerate: number,
channelCount: number,
codec: string,
manifestCodec: string
config: number[];
samplerate: number;
channelCount: number;
codec: string;
manifestCodec: string;
};
type FrameHeader = {
headerLength: number,
frameLength: number,
stamp: number
headerLength: number;
frameLength: number;
stamp: number;
};
export function getAudioConfig (observer, data: Uint8Array, offset: number, audioCodec: string): AudioConfig | void {
export function getAudioConfig(observer, data: Uint8Array, offset: number, audioCodec: string): AudioConfig | void {
let adtsObjectType: number;
let adtsExtensionSampleingIndex: number;
let adtsChanelConfig: number;
let config: number[];
const userAgent = navigator.userAgent.toLowerCase();
const manifestCodec = audioCodec;
const adtsSampleingRates = [
96000, 88200,
64000, 48000,
44100, 32000,
24000, 22050,
16000, 12000,
11025, 8000,
7350];
const adtsSampleingRates = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
// byte 2
adtsObjectType = ((data[offset + 2] & 0xC0) >>> 6) + 1;
const adtsSampleingIndex = ((data[offset + 2] & 0x3C) >>> 2);
adtsObjectType = ((data[offset + 2] & 0xc0) >>> 6) + 1;
const adtsSampleingIndex = (data[offset + 2] & 0x3c) >>> 2;
if (adtsSampleingIndex > adtsSampleingRates.length - 1) {
observer.trigger(Events.ERROR, { type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.FRAG_PARSING_ERROR, fatal: true, reason: `invalid ADTS sampling index:${adtsSampleingIndex}` });
observer.trigger(Events.ERROR, {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.FRAG_PARSING_ERROR,
fatal: true,
reason: `invalid ADTS sampling index:${adtsSampleingIndex}`,
});
return;
}
adtsChanelConfig = ((data[offset + 2] & 0x01) << 2);
adtsChanelConfig = (data[offset + 2] & 0x01) << 2;
// byte 3
adtsChanelConfig |= ((data[offset + 3] & 0xC0) >>> 6);
logger.log(`manifest codec:${audioCodec},ADTS data:type:${adtsObjectType},sampleingIndex:${adtsSampleingIndex}[${adtsSampleingRates[adtsSampleingIndex]}Hz],channelConfig:${adtsChanelConfig}`);
adtsChanelConfig |= (data[offset + 3] & 0xc0) >>> 6;
logger.log(
`manifest codec:${audioCodec},ADTS data:type:${adtsObjectType},sampleingIndex:${adtsSampleingIndex}[${adtsSampleingRates[adtsSampleingIndex]}Hz],channelConfig:${adtsChanelConfig}`
);
// firefox: freq less than 24kHz = AAC SBR (HE-AAC)
if (/firefox/i.test(userAgent)) {
if (adtsSampleingIndex >= 6) {
@@ -74,9 +74,7 @@ export function getAudioConfig (observer, data: Uint8Array, offset: number, audi
adtsObjectType = 5;
config = new Array(4);
// if (manifest codec is HE-AAC or HE-AACv2) OR (manifest codec not specified AND frequency less than 24kHz)
if ((audioCodec && ((audioCodec.indexOf('mp4a.40.29') !== -1) ||
(audioCodec.indexOf('mp4a.40.5') !== -1))) ||
(!audioCodec && adtsSampleingIndex >= 6)) {
if ((audioCodec && (audioCodec.indexOf('mp4a.40.29') !== -1 || audioCodec.indexOf('mp4a.40.5') !== -1)) || (!audioCodec && adtsSampleingIndex >= 6)) {
// HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies
// there is a factor 2 between frame sample rate and output sample rate
// multiply frequency by 2 (see table below, equivalent to substract 3)
@@ -84,9 +82,10 @@ export function getAudioConfig (observer, data: Uint8Array, offset: number, audi
} else {
// if (manifest codec is AAC) AND (frequency less than 24kHz AND nb channel is 1) OR (manifest codec not specified and mono audio)
// Chrome fails to play back with low frequency AAC LC mono when initialized with HE-AAC. This is not a problem with stereo.
if ((audioCodec && audioCodec.indexOf('mp4a.40.2') !== -1 &&
((adtsSampleingIndex >= 6 && adtsChanelConfig === 1) || /vivaldi/i.test(userAgent))) ||
(!audioCodec && adtsChanelConfig === 1)) {
if (
(audioCodec && audioCodec.indexOf('mp4a.40.2') !== -1 && ((adtsSampleingIndex >= 6 && adtsChanelConfig === 1) || /vivaldi/i.test(userAgent))) ||
(!audioCodec && adtsChanelConfig === 1)
) {
adtsObjectType = 2;
config = new Array(2);
}
@@ -129,13 +128,13 @@ export function getAudioConfig (observer, data: Uint8Array, offset: number, audi
// audioObjectType = profile => profile, the MPEG-4 Audio Object Type minus 1
config[0] = adtsObjectType << 3;
// samplingFrequencyIndex
config[0] |= (adtsSampleingIndex & 0x0E) >> 1;
config[0] |= (adtsSampleingIndex & 0x0e) >> 1;
config[1] |= (adtsSampleingIndex & 0x01) << 7;
// channelConfiguration
config[1] |= adtsChanelConfig << 3;
if (adtsObjectType === 5) {
// adtsExtensionSampleingIndex
config[1] |= (adtsExtensionSampleingIndex & 0x0E) >> 1;
config[1] |= (adtsExtensionSampleingIndex & 0x0e) >> 1;
config[2] = (adtsExtensionSampleingIndex & 0x01) << 7;
// adtsObjectType (force to 2, chrome is checking that object type is less than 5 ???
// https://chromium.googlesource.com/chromium/src.git/+/master/media/formats/mp4/aac.cc
@@ -146,45 +145,39 @@ export function getAudioConfig (observer, data: Uint8Array, offset: number, audi
config,
samplerate: adtsSampleingRates[adtsSampleingIndex],
channelCount: adtsChanelConfig,
codec: ('mp4a.40.' + adtsObjectType),
manifestCodec
codec: 'mp4a.40.' + adtsObjectType,
manifestCodec,
};
}
export function isHeaderPattern (data: Uint8Array, offset: number): boolean {
export function isHeaderPattern(data: Uint8Array, offset: number): boolean {
return data[offset] === 0xff && (data[offset + 1] & 0xf6) === 0xf0;
}
export function getHeaderLength (data: Uint8Array, offset: number): number {
return (data[offset + 1] & 0x01 ? 7 : 9);
export function getHeaderLength(data: Uint8Array, offset: number): number {
return data[offset + 1] & 0x01 ? 7 : 9;
}
export function getFullFrameLength (data: Uint8Array, offset: number): number {
return ((data[offset + 3] & 0x03) << 11) |
(data[offset + 4] << 3) |
((data[offset + 5] & 0xE0) >>> 5);
export function getFullFrameLength(data: Uint8Array, offset: number): number {
return ((data[offset + 3] & 0x03) << 11) | (data[offset + 4] << 3) | ((data[offset + 5] & 0xe0) >>> 5);
}
export function canGetFrameLength (data: Uint8Array, offset: number): boolean {
export function canGetFrameLength(data: Uint8Array, offset: number): boolean {
return offset + 5 < data.length;
}
export function isHeader (data: Uint8Array, offset: number): boolean {
export function isHeader(data: Uint8Array, offset: number): boolean {
// Look for ADTS header | 1111 1111 | 1111 X00X | where X can be either 0 or 1
// Layer bits (position 14 and 15) in header should be always 0 for ADTS
// More info https://wiki.multimedia.cx/index.php?title=ADTS
return offset + 1 < data.length && isHeaderPattern(data, offset);
}
export function canParse (data: Uint8Array, offset: number): boolean {
return (
canGetFrameLength(data, offset) &&
isHeaderPattern(data, offset) &&
getFullFrameLength(data, offset) < data.length - offset
);
export function canParse(data: Uint8Array, offset: number): boolean {
return canGetFrameLength(data, offset) && isHeaderPattern(data, offset) && getFullFrameLength(data, offset) < data.length - offset;
}
export function probe (data: Uint8Array, offset: number): boolean {
export function probe(data: Uint8Array, offset: number): boolean {
// same as isHeader but we also check that ADTS frame follows last ADTS frame
// or end of data is reached
if (isHeader(data, offset)) {
@@ -205,7 +198,7 @@ export function probe (data: Uint8Array, offset: number): boolean {
return false;
}
export function initTrackConfig (track: DemuxedAudioTrack, observer: HlsEventEmitter, data: Uint8Array, offset: number, audioCodec: string) {
export function initTrackConfig(track: DemuxedAudioTrack, observer: HlsEventEmitter, data: Uint8Array, offset: number, audioCodec: string) {
if (!track.samplerate) {
const config = getAudioConfig(observer, data, offset, audioCodec);
if (!config) {
@@ -220,11 +213,11 @@ export function initTrackConfig (track: DemuxedAudioTrack, observer: HlsEventEmi
}
}
export function getFrameDuration (samplerate: number): number {
return 1024 * 90000 / samplerate;
export function getFrameDuration(samplerate: number): number {
return (1024 * 90000) / samplerate;
}
export function parseFrameHeader (data: Uint8Array, offset: number, pts: number, frameIndex: number, frameDuration: number): FrameHeader | void {
export function parseFrameHeader(data: Uint8Array, offset: number, pts: number, frameIndex: number, frameDuration: number): FrameHeader | void {
const length = data.length;
// The protection skip bit tells us if we have 2 bytes of CRC data at the end of the ADTS header
@@ -233,14 +226,14 @@ export function parseFrameHeader (data: Uint8Array, offset: number, pts: number,
let frameLength = getFullFrameLength(data, offset);
frameLength -= headerLength;
if ((frameLength > 0) && ((offset + headerLength + frameLength) <= length)) {
if (frameLength > 0 && offset + headerLength + frameLength <= length) {
const stamp = pts + frameIndex * frameDuration;
// logger.log(`AAC frame, offset/length/total/pts:${offset+headerLength}/${frameLength}/${data.byteLength}/${(stamp/90).toFixed(0)}`);
return { headerLength, frameLength, stamp };
}
}
export function appendFrame (track: DemuxedAudioTrack, data: Uint8Array, offset: number, pts: number, frameIndex: number): AppendedAudioFrame | void {
export function appendFrame(track: DemuxedAudioTrack, data: Uint8Array, offset: number, pts: number, frameIndex: number): AppendedAudioFrame | void {
const frameDuration = getFrameDuration(track.samplerate as number);
const header = parseFrameHeader(data, offset, pts, frameIndex, frameDuration);
if (header) {
@@ -252,7 +245,7 @@ export function appendFrame (track: DemuxedAudioTrack, data: Uint8Array, offset:
const aacSample = {
unit: data.subarray(offset + headerLength, offset + headerLength + frameLength),
pts: stamp,
dts: stamp
dts: stamp,
};
track.samples.push(aacSample);
+12 -14
View File
@@ -11,7 +11,7 @@ class BaseAudioDemuxer implements Demuxer {
protected cachedData: Uint8Array | null = null;
protected initPTS: number | null = null;
resetInitSegment (audioCodec: string, videoCodec: string, duration: number) {
resetInitSegment(audioCodec: string, videoCodec: string, duration: number) {
this._id3Track = {
type: 'id3',
id: 0,
@@ -19,24 +19,22 @@ class BaseAudioDemuxer implements Demuxer {
inputTimeScale: 90000,
sequenceNumber: 0,
samples: [],
dropped: 0
dropped: 0,
};
}
resetTimeStamp () {
}
resetTimeStamp() {}
resetContiguity (): void {
}
resetContiguity(): void {}
canParse (data: Uint8Array, offset: number): boolean {
canParse(data: Uint8Array, offset: number): boolean {
return false;
}
appendFrame (track: DemuxedAudioTrack, data: Uint8Array, offset: number): AppendedAudioFrame | void {}
appendFrame(track: DemuxedAudioTrack, data: Uint8Array, offset: number): AppendedAudioFrame | void {}
// feed incoming data to the front of the parsing pipeline
demux (data: Uint8Array, timeOffset: number): DemuxerResult {
demux(data: Uint8Array, timeOffset: number): DemuxerResult {
if (this.cachedData) {
data = appendUint8Array(this.cachedData, data);
this.cachedData = null;
@@ -96,15 +94,15 @@ class BaseAudioDemuxer implements Demuxer {
audioTrack: track,
avcTrack: dummyTrack(),
id3Track,
textTrack: dummyTrack()
textTrack: dummyTrack(),
};
}
demuxSampleAes (data: Uint8Array, decryptData: Uint8Array, timeOffset: number): Promise<DemuxerResult> {
demuxSampleAes(data: Uint8Array, decryptData: Uint8Array, timeOffset: number): Promise<DemuxerResult> {
return Promise.reject(new Error(`[${this}] This demuxer does not support Sample-AES decryption`));
}
flush (timeOffset: number): DemuxerResult {
flush(timeOffset: number): DemuxerResult {
// Parse cache in case of remaining frames.
if (this.cachedData) {
this.demux(this.cachedData, 0);
@@ -118,11 +116,11 @@ class BaseAudioDemuxer implements Demuxer {
audioTrack: this._audioTrack,
avcTrack: dummyTrack(),
id3Track: this._id3Track,
textTrack: dummyTrack()
textTrack: dummyTrack(),
};
}
destroy () {}
destroy() {}
}
/**
+4 -4
View File
@@ -2,12 +2,12 @@ export default class ChunkCache {
private chunks: Array<Uint8Array> = [];
public dataLength: number = 0;
push (chunk: Uint8Array) {
push(chunk: Uint8Array) {
this.chunks.push(chunk);
this.dataLength += chunk.length;
}
flush (): Uint8Array {
flush(): Uint8Array {
const { chunks, dataLength } = this;
let result;
if (!chunks.length) {
@@ -21,13 +21,13 @@ export default class ChunkCache {
return result;
}
reset () {
reset() {
this.chunks.length = 0;
this.dataLength = 0;
}
}
function concatUint8Arrays (chunks: Array<Uint8Array>, dataLength: number) : Uint8Array {
function concatUint8Arrays(chunks: Array<Uint8Array>, dataLength: number): Uint8Array {
const result = new Uint8Array(dataLength);
let offset = 0;
for (let i = 0; i < chunks.length; i++) {
+2 -2
View File
@@ -1,6 +1,6 @@
import type { DemuxedTrack } from '../types/demuxer';
export function dummyTrack (): DemuxedTrack {
export function dummyTrack(): DemuxedTrack {
return {
type: '',
id: -1,
@@ -8,6 +8,6 @@ export function dummyTrack (): DemuxedTrack {
inputTimeScale: 90000,
sequenceNumber: -1,
samples: [],
dropped: 0
dropped: 0,
};
}
+93 -56
View File
@@ -1,11 +1,11 @@
/**
* Parser for exponential Golomb codes, a variable-bitwidth number encoding scheme used by h264.
*/
*/
import { logger } from '../utils/logger';
class ExpGolomb {
constructor (data) {
constructor(data) {
this.data = data;
// the number of bytes left to examine in this.data
this.bytesAvailable = data.byteLength;
@@ -16,7 +16,7 @@ class ExpGolomb {
}
// ():void
loadWord () {
loadWord() {
const data = this.data;
const bytesAvailable = this.bytesAvailable;
const position = data.byteLength - bytesAvailable;
@@ -34,7 +34,7 @@ class ExpGolomb {
}
// (count:int):void
skipBits (count) {
skipBits(count) {
let skipBytes; // :int
if (this.bitsAvailable > count) {
this.word <<= count;
@@ -42,7 +42,7 @@ class ExpGolomb {
} else {
count -= this.bitsAvailable;
skipBytes = count >> 3;
count -= (skipBytes >> 3);
count -= skipBytes >> 3;
this.bytesAvailable -= skipBytes;
this.loadWord();
this.word <<= count;
@@ -51,7 +51,7 @@ class ExpGolomb {
}
// (size:int):uint
readBits (size) {
readBits(size) {
let bits = Math.min(this.bitsAvailable, size); // :uint
const valu = this.word >>> (32 - bits); // :uint
if (size > 32) {
@@ -67,14 +67,14 @@ class ExpGolomb {
bits = size - bits;
if (bits > 0 && this.bitsAvailable) {
return valu << bits | this.readBits(bits);
return (valu << bits) | this.readBits(bits);
} else {
return valu;
}
}
// ():uint
skipLZ () {
skipLZ() {
let leadingZeroCount; // :uint
for (leadingZeroCount = 0; leadingZeroCount < this.bitsAvailable; ++leadingZeroCount) {
if ((this.word & (0x80000000 >>> leadingZeroCount)) !== 0) {
@@ -90,23 +90,23 @@ class ExpGolomb {
}
// ():void
skipUEG () {
skipUEG() {
this.skipBits(1 + this.skipLZ());
}
// ():void
skipEG () {
skipEG() {
this.skipBits(1 + this.skipLZ());
}
// ():uint
readUEG () {
readUEG() {
const clz = this.skipLZ(); // :uint
return this.readBits(clz + 1) - 1;
}
// ():int
readEG () {
readEG() {
const valu = this.readUEG(); // :int
if (0x01 & valu) {
// the number is odd if the low order bit is set
@@ -118,22 +118,22 @@ class ExpGolomb {
// Some convenience functions
// :Boolean
readBoolean () {
readBoolean() {
return this.readBits(1) === 1;
}
// ():int
readUByte () {
readUByte() {
return this.readBits(8);
}
// ():int
readUShort () {
readUShort() {
return this.readBits(16);
}
// ():int
readUInt () {
readUInt() {
return this.readBits(32);
}
@@ -144,7 +144,7 @@ class ExpGolomb {
* @param count {number} the number of entries in this scaling list
* @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1
*/
skipScalingList (count) {
skipScalingList(count) {
let lastScale = 8;
let nextScale = 8;
let deltaScale;
@@ -153,7 +153,7 @@ class ExpGolomb {
deltaScale = this.readEG();
nextScale = (lastScale + deltaScale + 256) % 256;
}
lastScale = (nextScale === 0) ? lastScale : nextScale;
lastScale = nextScale === 0 ? lastScale : nextScale;
}
}
@@ -166,7 +166,7 @@ class ExpGolomb {
* sequence parameter set, including the dimensions of the
* associated video frames.
*/
readSPS () {
readSPS() {
let frameCropLeftOffset = 0;
let frameCropRightOffset = 0;
let frameCropTopOffset = 0;
@@ -190,15 +190,17 @@ class ExpGolomb {
readUByte(); // level_idc u(8)
skipUEG(); // seq_parameter_set_id
// some profiles have more optional data we don't need
if (profileIdc === 100 ||
profileIdc === 110 ||
profileIdc === 122 ||
profileIdc === 244 ||
profileIdc === 44 ||
profileIdc === 83 ||
profileIdc === 86 ||
profileIdc === 118 ||
profileIdc === 128) {
if (
profileIdc === 100 ||
profileIdc === 110 ||
profileIdc === 122 ||
profileIdc === 244 ||
profileIdc === 44 ||
profileIdc === 83 ||
profileIdc === 86 ||
profileIdc === 118 ||
profileIdc === 128
) {
const chromaFormatIdc = readUEG();
if (chromaFormatIdc === 3) {
skipBits(1);
@@ -207,10 +209,12 @@ class ExpGolomb {
skipUEG(); // bit_depth_luma_minus8
skipUEG(); // bit_depth_chroma_minus8
skipBits(1); // qpprime_y_zero_transform_bypass_flag
if (readBoolean()) { // seq_scaling_matrix_present_flag
scalingListCount = (chromaFormatIdc !== 3) ? 8 : 12;
if (readBoolean()) {
// seq_scaling_matrix_present_flag
scalingListCount = chromaFormatIdc !== 3 ? 8 : 12;
for (i = 0; i < scalingListCount; i++) {
if (readBoolean()) { // seq_scaling_list_present_flag[ i ]
if (readBoolean()) {
// seq_scaling_list_present_flag[ i ]
if (i < 6) {
skipScalingList(16);
} else {
@@ -243,7 +247,8 @@ class ExpGolomb {
} // mb_adaptive_frame_field_flag
skipBits(1); // direct_8x8_inference_flag
if (readBoolean()) { // frame_cropping_flag
if (readBoolean()) {
// frame_cropping_flag
frameCropLeftOffset = readUEG();
frameCropRightOffset = readUEG();
frameCropTopOffset = readUEG();
@@ -256,37 +261,69 @@ class ExpGolomb {
// aspect_ratio_info_present_flag
const aspectRatioIdc = readUByte();
switch (aspectRatioIdc) {
case 1: pixelRatio = [1, 1]; break;
case 2: pixelRatio = [12, 11]; break;
case 3: pixelRatio = [10, 11]; break;
case 4: pixelRatio = [16, 11]; break;
case 5: pixelRatio = [40, 33]; break;
case 6: pixelRatio = [24, 11]; break;
case 7: pixelRatio = [20, 11]; break;
case 8: pixelRatio = [32, 11]; break;
case 9: pixelRatio = [80, 33]; break;
case 10: pixelRatio = [18, 11]; break;
case 11: pixelRatio = [15, 11]; break;
case 12: pixelRatio = [64, 33]; break;
case 13: pixelRatio = [160, 99]; break;
case 14: pixelRatio = [4, 3]; break;
case 15: pixelRatio = [3, 2]; break;
case 16: pixelRatio = [2, 1]; break;
case 255: {
pixelRatio = [readUByte() << 8 | readUByte(), readUByte() << 8 | readUByte()];
break;
}
case 1:
pixelRatio = [1, 1];
break;
case 2:
pixelRatio = [12, 11];
break;
case 3:
pixelRatio = [10, 11];
break;
case 4:
pixelRatio = [16, 11];
break;
case 5:
pixelRatio = [40, 33];
break;
case 6:
pixelRatio = [24, 11];
break;
case 7:
pixelRatio = [20, 11];
break;
case 8:
pixelRatio = [32, 11];
break;
case 9:
pixelRatio = [80, 33];
break;
case 10:
pixelRatio = [18, 11];
break;
case 11:
pixelRatio = [15, 11];
break;
case 12:
pixelRatio = [64, 33];
break;
case 13:
pixelRatio = [160, 99];
break;
case 14:
pixelRatio = [4, 3];
break;
case 15:
pixelRatio = [3, 2];
break;
case 16:
pixelRatio = [2, 1];
break;
case 255: {
pixelRatio = [(readUByte() << 8) | readUByte(), (readUByte() << 8) | readUByte()];
break;
}
}
}
}
return {
width: Math.ceil((((picWidthInMbsMinus1 + 1) * 16) - frameCropLeftOffset * 2 - frameCropRightOffset * 2)),
height: ((2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16) - ((frameMbsOnlyFlag ? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset)),
pixelRatio: pixelRatio
width: Math.ceil((picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2),
height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - (frameMbsOnlyFlag ? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset),
pixelRatio: pixelRatio,
};
}
readSliceType () {
readSliceType() {
// skip NALu type
this.readUByte();
// discard first_mb_in_slice
+51 -48
View File
@@ -1,7 +1,7 @@
type RawFrame = {type: string, size: number, data: Uint8Array};
type RawFrame = { type: string; size: number; data: Uint8Array };
// breaking up those two types in order to clarify what is happening in the decoding path.
type DecodedFrame<T> = {key: string, data: T, info?: any};
type DecodedFrame<T> = { key: string; data: T; info?: any };
export type Frame = DecodedFrame<ArrayBuffer | string>;
/**
@@ -12,23 +12,23 @@ export type Frame = DecodedFrame<ArrayBuffer | string>;
*/
export const isHeader = (data: Uint8Array, offset: number): boolean => {
/*
* http://id3.org/id3v2.3.0
* [0] = 'I'
* [1] = 'D'
* [2] = '3'
* [3,4] = {Version}
* [5] = {Flags}
* [6-9] = {ID3 Size}
*
* An ID3v2 tag can be detected with the following pattern:
* $49 44 33 yy yy xx zz zz zz zz
* Where yy is less than $FF, xx is the 'flags' byte and zz is less than $80
*/
* http://id3.org/id3v2.3.0
* [0] = 'I'
* [1] = 'D'
* [2] = '3'
* [3,4] = {Version}
* [5] = {Flags}
* [6-9] = {ID3 Size}
*
* An ID3v2 tag can be detected with the following pattern:
* $49 44 33 yy yy xx zz zz zz zz
* Where yy is less than $FF, xx is the 'flags' byte and zz is less than $80
*/
if (offset + 10 <= data.length) {
// look for 'ID3' identifier
if (data[offset] === 0x49 && data[offset + 1] === 0x44 && data[offset + 2] === 0x33) {
// check version is within range
if (data[offset + 3] < 0xFF && data[offset + 4] < 0xFF) {
if (data[offset + 3] < 0xff && data[offset + 4] < 0xff) {
// check size is within range
if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80) {
return true;
@@ -48,13 +48,13 @@ export const isHeader = (data: Uint8Array, offset: number): boolean => {
*/
export const isFooter = (data: Uint8Array, offset: number): boolean => {
/*
* The footer is a copy of the header, but with a different identifier
*/
* The footer is a copy of the header, but with a different identifier
*/
if (offset + 10 <= data.length) {
// look for '3DI' identifier
if (data[offset] === 0x33 && data[offset + 1] === 0x44 && data[offset + 2] === 0x49) {
// check version is within range
if (data[offset + 3] < 0xFF && data[offset + 4] < 0xFF) {
if (data[offset + 3] < 0xff && data[offset + 4] < 0xff) {
// check size is within range
if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80) {
return true;
@@ -101,10 +101,10 @@ export const getID3Data = (data: Uint8Array, offset: number): Uint8Array | undef
const readSize = (data: Uint8Array, offset: number): number => {
let size = 0;
size = ((data[offset] & 0x7f) << 21);
size |= ((data[offset + 1] & 0x7f) << 14);
size |= ((data[offset + 2] & 0x7f) << 7);
size |= (data[offset + 3] & 0x7f);
size = (data[offset] & 0x7f) << 21;
size |= (data[offset + 1] & 0x7f) << 14;
size |= (data[offset + 2] & 0x7f) << 7;
size |= data[offset + 3] & 0x7f;
return size;
};
@@ -136,7 +136,7 @@ export const getTimeStamp = (data: Uint8Array): number | undefined => {
* @param {ID3 frame} frame
*/
export const isTimeStampFrame = (frame: Frame): boolean => {
return (frame && frame.key === 'PRIV' && frame.info === 'com.apple.streaming.transportStreamTimestamp');
return frame && frame.key === 'PRIV' && frame.info === 'com.apple.streaming.transportStreamTimestamp';
};
const getFrameData = (data: Uint8Array): RawFrame => {
@@ -273,10 +273,7 @@ const readTimeStamp = (timeStampFrame: DecodedFrame<ArrayBuffer>): number | unde
// timestamp is 33 bit expressed as a big-endian eight-octet number,
// with the upper 31 bits set to zero.
const pts33Bit = data[3] & 0x1;
let timestamp = (data[4] << 23) +
(data[5] << 15) +
(data[6] << 7) +
data[7];
let timestamp = (data[4] << 23) + (data[5] << 15) + (data[6] << 7) + data[7];
timestamp /= 45;
if (pts33Bit) {
@@ -298,7 +295,7 @@ const readTimeStamp = (timeStampFrame: DecodedFrame<ArrayBuffer>): number | unde
* LastModified: Dec 25 1999
* This library is free. You can redistribute it and/or modify it.
*/
export const utf8ArrayToStr = (array: Uint8Array, exitOnNull:boolean = false): string => {
export const utf8ArrayToStr = (array: Uint8Array, exitOnNull: boolean = false): string => {
const decoder = getTextDecoder();
if (decoder) {
const decoded = decoder.decode(array);
@@ -328,36 +325,42 @@ export const utf8ArrayToStr = (array: Uint8Array, exitOnNull:boolean = false): s
continue;
}
switch (c >> 4) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12: case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
break;
default:
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
// 0xxxxxxx
out += String.fromCharCode(c);
break;
case 12:
case 13:
// 110x xxxx 10xx xxxx
char2 = array[i++];
out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
break;
case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0));
break;
default:
}
}
return out;
};
export const testables = {
decodeTextFrame: decodeTextFrame
decodeTextFrame: decodeTextFrame,
};
let decoder: TextDecoder;
function getTextDecoder () {
function getTextDecoder() {
if (!decoder && typeof self.TextDecoder !== 'undefined') {
decoder = new self.TextDecoder('utf-8');
}
+5 -5
View File
@@ -9,7 +9,7 @@ import * as MpegAudio from './mpegaudio';
class MP3Demuxer extends BaseAudioDemuxer {
static readonly minProbeByteLength: number = 4;
resetInitSegment (audioCodec, videoCodec, duration) {
resetInitSegment(audioCodec, videoCodec, duration) {
super.resetInitSegment(audioCodec, videoCodec, duration);
this._audioTrack = {
container: 'audio/mpeg',
@@ -22,11 +22,11 @@ class MP3Demuxer extends BaseAudioDemuxer {
manifestCodec: audioCodec,
duration: duration,
inputTimeScale: 90000,
dropped: 0
dropped: 0,
};
}
static probe (data): boolean {
static probe(data): boolean {
if (!data) {
return false;
}
@@ -47,11 +47,11 @@ class MP3Demuxer extends BaseAudioDemuxer {
return false;
}
canParse (data, offset) {
canParse(data, offset) {
return MpegAudio.canParse(data, offset);
}
appendFrame (track, data, offset) {
appendFrame(track, data, offset) {
if (this.initPTS === null) {
return;
}
+11 -14
View File
@@ -12,25 +12,22 @@ class MP4Demuxer implements Demuxer {
private remainderData: Uint8Array | null = null;
private config: HlsConfig;
constructor (observer: HlsEventEmitter, config: HlsConfig) {
constructor(observer: HlsEventEmitter, config: HlsConfig) {
this.config = config;
}
resetTimeStamp () {
}
resetTimeStamp() {}
resetInitSegment () {
}
resetInitSegment() {}
resetContiguity (): void {
}
resetContiguity(): void {}
static probe (data) {
static probe(data) {
// ensure we find a moof box in the first 16 kB
return findBox({ data: data, start: 0, end: Math.min(data.length, 16384) }, ['moof']).length > 0;
}
demux (data): DemuxerResult {
demux(data): DemuxerResult {
// Load all data into the avc track. The CMAF remuxer will look for the data in the samples object; the rest of the fields do not matter
let avcSamples = data;
const avcTrack = dummyTrack();
@@ -52,11 +49,11 @@ class MP4Demuxer implements Demuxer {
audioTrack: dummyTrack(),
avcTrack,
id3Track: dummyTrack(),
textTrack: dummyTrack()
textTrack: dummyTrack(),
};
}
flush () {
flush() {
const avcTrack: DemuxedTrack = dummyTrack();
avcTrack.samples = this.remainderData;
this.remainderData = null;
@@ -65,15 +62,15 @@ class MP4Demuxer implements Demuxer {
audioTrack: dummyTrack(),
avcTrack,
id3Track: dummyTrack(),
textTrack: dummyTrack()
textTrack: dummyTrack(),
};
}
demuxSampleAes (data: Uint8Array, decryptData: Uint8Array, timeOffset: number): Promise<DemuxerResult> {
demuxSampleAes(data: Uint8Array, decryptData: Uint8Array, timeOffset: number): Promise<DemuxerResult> {
return Promise.reject(new Error('The MP4 demuxer does not support SAMPLE-AES decryption'));
}
destroy () {}
destroy() {}
}
export default MP4Demuxer;
+86 -23
View File
@@ -1,18 +1,81 @@
/**
* MPEG parser helper
*/
import {
DemuxedAudioTrack
} from '../types/demuxer';
import { DemuxedAudioTrack } from '../types/demuxer';
let chromeVersion: number | null = null;
const BitratesMap = [
32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448,
32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384,
32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320,
32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256,
8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160
32,
64,
96,
128,
160,
192,
224,
256,
288,
320,
352,
384,
416,
448,
32,
48,
56,
64,
80,
96,
112,
128,
160,
192,
224,
256,
320,
384,
32,
40,
48,
56,
64,
80,
96,
112,
128,
160,
192,
224,
256,
320,
32,
48,
56,
64,
80,
96,
112,
128,
144,
160,
176,
192,
224,
256,
8,
16,
24,
32,
40,
48,
56,
64,
80,
96,
112,
128,
144,
160,
];
const SamplingRateMap = [44100, 48000, 32000, 22050, 24000, 16000, 11025, 12000, 8000];
@@ -23,39 +86,39 @@ const SamplesCoefficients = [
0, // Reserved
72, // Layer3
144, // Layer2
12 // Layer1
12, // Layer1
],
// Reserved
[
0, // Reserved
0, // Layer3
0, // Layer2
0 // Layer1
0, // Layer1
],
// MPEG 2
[
0, // Reserved
72, // Layer3
144, // Layer2
12 // Layer1
12, // Layer1
],
// MPEG 1
[
0, // Reserved
144, // Layer3
144, // Layer2
12 // Layer1
]
12, // Layer1
],
];
const BytesInSlot = [
0, // Reserved
1, // Layer3
1, // Layer2
4 // Layer1
4, // Layer1
];
export function appendFrame (track: DemuxedAudioTrack, data: Uint8Array, offset: number, pts: number, frameIndex: number) {
export function appendFrame(track: DemuxedAudioTrack, data: Uint8Array, offset: number, pts: number, frameIndex: number) {
// Using http://www.datavoyage.com/mpgscript/mpeghdr.htm as a reference
if (offset + 24 > data.length) {
return;
@@ -63,7 +126,7 @@ export function appendFrame (track: DemuxedAudioTrack, data: Uint8Array, offset:
const header = parseHeader(data, offset);
if (header && offset + header.frameLength <= data.length) {
const frameDuration = header.samplesPerFrame * 90000 / header.sampleRate;
const frameDuration = (header.samplesPerFrame * 90000) / header.sampleRate;
const stamp = pts + frameIndex * frameDuration;
const sample = { unit: data.subarray(offset, offset + header.frameLength), pts: stamp, dts: stamp };
@@ -76,7 +139,7 @@ export function appendFrame (track: DemuxedAudioTrack, data: Uint8Array, offset:
}
}
export function parseHeader (data: Uint8Array, offset: number) {
export function parseHeader(data: Uint8Array, offset: number) {
const mpegVersion = (data[offset + 1] >> 3) & 3;
const mpegLayer = (data[offset + 1] >> 1) & 3;
const bitRateIndex = (data[offset + 2] >> 4) & 15;
@@ -84,7 +147,7 @@ export function parseHeader (data: Uint8Array, offset: number) {
if (mpegVersion !== 1 && bitRateIndex !== 0 && bitRateIndex !== 15 && sampleRateIndex !== 3) {
const paddingBit = (data[offset + 2] >> 1) & 1;
const channelMode = data[offset + 3] >> 6;
const columnInBitrates = mpegVersion === 3 ? (3 - mpegLayer) : (mpegLayer === 3 ? 3 : 4);
const columnInBitrates = mpegVersion === 3 ? 3 - mpegLayer : mpegLayer === 3 ? 3 : 4;
const bitRate = BitratesMap[columnInBitrates * 14 + bitRateIndex - 1] * 1000;
const columnInSampleRates = mpegVersion === 3 ? 0 : mpegVersion === 2 ? 1 : 2;
const sampleRate = SamplingRateMap[columnInSampleRates * 3 + sampleRateIndex];
@@ -92,7 +155,7 @@ export function parseHeader (data: Uint8Array, offset: number) {
const sampleCoefficient = SamplesCoefficients[mpegVersion][mpegLayer];
const bytesInSlot = BytesInSlot[mpegLayer];
const samplesPerFrame = sampleCoefficient * 8 * bytesInSlot;
const frameLength = Math.floor(sampleCoefficient * bitRate / sampleRate + paddingBit) * bytesInSlot;
const frameLength = Math.floor((sampleCoefficient * bitRate) / sampleRate + paddingBit) * bytesInSlot;
if (chromeVersion === null) {
const userAgent = navigator.userAgent || '';
@@ -110,24 +173,24 @@ export function parseHeader (data: Uint8Array, offset: number) {
}
}
export function isHeaderPattern (data: Uint8Array, offset: number): boolean {
export function isHeaderPattern(data: Uint8Array, offset: number): boolean {
return data[offset] === 0xff && (data[offset + 1] & 0xe0) === 0xe0 && (data[offset + 1] & 0x06) !== 0x00;
}
export function isHeader (data: Uint8Array, offset: number): boolean {
export function isHeader(data: Uint8Array, offset: number): boolean {
// Look for MPEG header | 1111 1111 | 111X XYZX | where X can be either 0 or 1 and Y or Z should be 1
// Layer bits (position 14 and 15) in header should be always different from 0 (Layer I or Layer II or Layer III)
// More info http://www.mp3-tech.org/programmer/frame_header.html
return offset + 1 < data.length && isHeaderPattern(data, offset);
}
export function canParse (data: Uint8Array, offset: number): boolean {
export function canParse(data: Uint8Array, offset: number): boolean {
const headerSize = 4;
return isHeaderPattern(data, offset) && data.length - offset >= headerSize;
}
export function probe (data: Uint8Array, offset: number): boolean {
export function probe(data: Uint8Array, offset: number): boolean {
// same as isHeader but we also check that MPEG frame follows last MPEG frame
// or end of data is reached
if (offset + 1 < data.length && isHeaderPattern(data, offset)) {
+14 -16
View File
@@ -1,27 +1,25 @@
/**
* SAMPLE-AES decrypter
*/
*/
import Decrypter from '../crypt/decrypter';
class SampleAesDecrypter {
constructor (observer, config, decryptdata, discardEPB) {
constructor(observer, config, decryptdata, discardEPB) {
this.decryptdata = decryptdata;
this.discardEPB = discardEPB;
this.decrypter = new Decrypter(observer, config, { removePKCS7Padding: false });
}
decryptBuffer (encryptedData, callback) {
decryptBuffer(encryptedData, callback) {
this.decrypter.decrypt(encryptedData, this.decryptdata.key.buffer, this.decryptdata.iv.buffer, callback);
}
// AAC - encrypt all full 16 bytes blocks starting from offset 16
decryptAacSample (samples, sampleIndex, callback, sync) {
decryptAacSample(samples, sampleIndex, callback, sync) {
const curUnit = samples[sampleIndex].unit;
const encryptedData = curUnit.subarray(16, curUnit.length - curUnit.length % 16);
const encryptedBuffer = encryptedData.buffer.slice(
encryptedData.byteOffset,
encryptedData.byteOffset + encryptedData.length);
const encryptedData = curUnit.subarray(16, curUnit.length - (curUnit.length % 16));
const encryptedBuffer = encryptedData.buffer.slice(encryptedData.byteOffset, encryptedData.byteOffset + encryptedData.length);
const localthis = this;
this.decryptBuffer(encryptedBuffer, function (decryptedData) {
@@ -34,8 +32,8 @@ class SampleAesDecrypter {
});
}
decryptAacSamples (samples, sampleIndex, callback) {
for (;; sampleIndex++) {
decryptAacSamples(samples, sampleIndex, callback) {
for (; ; sampleIndex++) {
if (sampleIndex >= samples.length) {
callback();
return;
@@ -56,7 +54,7 @@ class SampleAesDecrypter {
}
// AVC - encrypt one 16 bytes block out of ten, starting from offset 32
getAvcEncryptedData (decodedData) {
getAvcEncryptedData(decodedData) {
const encryptedDataLen = Math.floor((decodedData.length - 48) / 160) * 16 + 16;
const encryptedData = new Int8Array(encryptedDataLen);
let outputPos = 0;
@@ -67,7 +65,7 @@ class SampleAesDecrypter {
return encryptedData;
}
getAvcDecryptedUnit (decodedData, decryptedData) {
getAvcDecryptedUnit(decodedData, decryptedData) {
decryptedData = new Uint8Array(decryptedData);
let inputPos = 0;
for (let outputPos = 32; outputPos <= decodedData.length - 16; outputPos += 160, inputPos += 16) {
@@ -77,7 +75,7 @@ class SampleAesDecrypter {
return decodedData;
}
decryptAvcSample (samples, sampleIndex, unitIndex, callback, curUnit, sync) {
decryptAvcSample(samples, sampleIndex, unitIndex, callback, curUnit, sync) {
const decodedData = this.discardEPB(curUnit.data);
const encryptedData = this.getAvcEncryptedData(decodedData);
const localthis = this;
@@ -91,15 +89,15 @@ class SampleAesDecrypter {
});
}
decryptAvcSamples (samples, sampleIndex, unitIndex, callback) {
for (;; sampleIndex++, unitIndex = 0) {
decryptAvcSamples(samples, sampleIndex, unitIndex, callback) {
for (; ; sampleIndex++, unitIndex = 0) {
if (sampleIndex >= samples.length) {
callback();
return;
}
const curUnits = samples[sampleIndex].units;
for (;; unitIndex++) {
for (; ; unitIndex++) {
if (unitIndex >= curUnits.length) {
break;
}
+66 -46
View File
@@ -25,7 +25,7 @@ export default class TransmuxerInterface {
private onTransmuxComplete: (transmuxResult: TransmuxerResult) => void;
private onFlush: (chunkMeta: ChunkMetadata) => void;
constructor (hls: Hls, id: PlaylistLevelType, onTransmuxComplete: (transmuxResult: TransmuxerResult) => void, onFlush: (chunkMeta: ChunkMetadata) => void) {
constructor(hls: Hls, id: PlaylistLevelType, onTransmuxComplete: (transmuxResult: TransmuxerResult) => void, onFlush: (chunkMeta: ChunkMetadata) => void) {
this.hls = hls;
this.id = id;
this.onTransmuxComplete = onTransmuxComplete;
@@ -48,12 +48,12 @@ export default class TransmuxerInterface {
const typeSupported = {
mp4: MediaSource.isTypeSupported('video/mp4'),
mpeg: MediaSource.isTypeSupported('audio/mpeg'),
mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"')
mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"'),
};
// navigator.vendor is not always available in Web Worker
// refer to https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/navigator
const vendor = navigator.vendor;
if (config.enableWorker && (typeof (Worker) !== 'undefined')) {
if (config.enableWorker && typeof Worker !== 'undefined') {
logger.log('demuxing in webworker');
let worker;
try {
@@ -61,7 +61,13 @@ export default class TransmuxerInterface {
this.onwmsg = this.onWorkerMessage.bind(this);
worker.addEventListener('message', this.onwmsg);
worker.onerror = (event) => {
hls.trigger(Events.ERROR, { type: ErrorTypes.OTHER_ERROR, details: ErrorDetails.INTERNAL_EXCEPTION, fatal: true, event: 'demuxerWorker', err: { message: event.message + ' (' + event.filename + ':' + event.lineno + ')' } });
hls.trigger(Events.ERROR, {
type: ErrorTypes.OTHER_ERROR,
details: ErrorDetails.INTERNAL_EXCEPTION,
fatal: true,
event: 'demuxerWorker',
err: { message: event.message + ' (' + event.filename + ':' + event.lineno + ')' },
});
};
worker.postMessage({ cmd: 'init', typeSupported: typeSupported, vendor: vendor, id: id, config: JSON.stringify(config) });
} catch (err) {
@@ -79,7 +85,7 @@ export default class TransmuxerInterface {
}
}
destroy (): void {
destroy(): void {
const w = this.worker;
if (w) {
w.removeEventListener('message', this.onwmsg);
@@ -100,17 +106,28 @@ export default class TransmuxerInterface {
this.observer = null;
}
push (data: ArrayBuffer, initSegmentData: Uint8Array, audioCodec: string | undefined, videoCodec: string | undefined, frag: Fragment, part: Part | null, duration: number, accurateTimeOffset: boolean, chunkMeta: ChunkMetadata, defaultInitPTS?: number): void {
push(
data: ArrayBuffer,
initSegmentData: Uint8Array,
audioCodec: string | undefined,
videoCodec: string | undefined,
frag: Fragment,
part: Part | null,
duration: number,
accurateTimeOffset: boolean,
chunkMeta: ChunkMetadata,
defaultInitPTS?: number
): void {
chunkMeta.transmuxing.start = self.performance.now();
const { transmuxer, worker } = this;
const timeOffset = part ? part.start : frag.start;
const decryptdata = frag.decryptdata;
const lastFrag = this.frag;
const discontinuity = !(lastFrag && (frag.cc === lastFrag.cc));
const trackSwitch = !(lastFrag && (chunkMeta.level === lastFrag.level));
const discontinuity = !(lastFrag && frag.cc === lastFrag.cc);
const trackSwitch = !(lastFrag && chunkMeta.level === lastFrag.level);
const snDiff = lastFrag ? chunkMeta.sn - (lastFrag.sn as number) : -1;
const partDiff = this.part ? (chunkMeta.part - this.part.index) : 1;
const partDiff = this.part ? chunkMeta.part - this.part.index : 1;
const contiguous = !trackSwitch && (snDiff === 1 || (snDiff === 0 && partDiff === 1));
const now = self.performance.now();
@@ -138,16 +155,19 @@ export default class TransmuxerInterface {
// Frags with sn of 'initSegment' are not transmuxed
if (worker) {
// post fragment payload as transferable objects for ArrayBuffer (no copy)
worker.postMessage({
cmd: 'demux',
data,
decryptdata,
chunkMeta
}, data instanceof ArrayBuffer ? [data] : []);
worker.postMessage(
{
cmd: 'demux',
data,
decryptdata,
chunkMeta,
},
data instanceof ArrayBuffer ? [data] : []
);
} else if (transmuxer) {
const transmuxResult = transmuxer.push(data, decryptdata, chunkMeta);
if (isPromise(transmuxResult)) {
transmuxResult.then(data => {
transmuxResult.then((data) => {
this.handleTransmuxComplete(data);
});
} else {
@@ -156,18 +176,18 @@ export default class TransmuxerInterface {
}
}
flush (chunkMeta: ChunkMetadata) {
flush(chunkMeta: ChunkMetadata) {
chunkMeta.transmuxing.start = self.performance.now();
const { transmuxer, worker } = this;
if (worker) {
worker.postMessage({
cmd: 'flush',
chunkMeta
chunkMeta,
});
} else if (transmuxer) {
const transmuxResult = transmuxer.flush(chunkMeta);
if (isPromise(transmuxResult)) {
transmuxResult.then(data => {
transmuxResult.then((data) => {
this.handleFlushResult(data, chunkMeta);
});
} else {
@@ -176,58 +196,58 @@ export default class TransmuxerInterface {
}
}
private handleFlushResult (results: Array<TransmuxerResult>, chunkMeta: ChunkMetadata) {
results.forEach(result => {
private handleFlushResult(results: Array<TransmuxerResult>, chunkMeta: ChunkMetadata) {
results.forEach((result) => {
this.handleTransmuxComplete(result);
});
this.onFlush(chunkMeta);
}
private onWorkerMessage (ev: any): void {
private onWorkerMessage(ev: any): void {
const data = ev.data;
const hls = this.hls;
switch (data.event) {
case 'init': {
// revoke the Object URL that was used to create transmuxer worker, so as not to leak it
self.URL.revokeObjectURL(this.worker.objectURL);
break;
}
case 'init': {
// revoke the Object URL that was used to create transmuxer worker, so as not to leak it
self.URL.revokeObjectURL(this.worker.objectURL);
break;
}
case 'transmuxComplete': {
this.handleTransmuxComplete(data.data);
break;
}
case 'transmuxComplete': {
this.handleTransmuxComplete(data.data);
break;
}
case 'flush': {
this.onFlush(data.data);
break;
}
case 'flush': {
this.onFlush(data.data);
break;
}
/* falls through */
default: {
data.data = data.data || {};
data.data.frag = this.frag;
data.data.id = this.id;
hls.trigger(data.event, data.data);
break;
}
/* falls through */
default: {
data.data = data.data || {};
data.data.frag = this.frag;
data.data.id = this.id;
hls.trigger(data.event, data.data);
break;
}
}
}
private configureTransmuxer (config: TransmuxConfig, state: TransmuxState) {
private configureTransmuxer(config: TransmuxConfig, state: TransmuxState) {
const { worker, transmuxer } = this;
if (worker) {
worker.postMessage({
cmd: 'configure',
config,
state
state,
});
} else if (transmuxer) {
transmuxer.configure(config, state);
}
}
private handleTransmuxComplete (result: TransmuxerResult) {
private handleTransmuxComplete(result: TransmuxerResult) {
result.chunkMeta.transmuxing.end = self.performance.now();
this.onTransmuxComplete(result);
}
+41 -46
View File
@@ -5,7 +5,7 @@ import { EventEmitter } from 'eventemitter3';
import type { RemuxedTrack, RemuxerResult } from '../types/remuxer';
import type { TransmuxerResult, ChunkMetadata } from '../types/transmuxer';
export default function TransmuxerWorker (self) {
export default function TransmuxerWorker(self) {
const observer = new EventEmitter();
const forwardMessage = (ev, data) => {
self.postMessage({ event: ev, data: data });
@@ -18,48 +18,47 @@ export default function TransmuxerWorker (self) {
self.addEventListener('message', (ev) => {
const data = ev.data;
switch (data.cmd) {
case 'init': {
const config = JSON.parse(data.config);
self.transmuxer = new Transmuxer(observer, data.typeSupported, config, data.vendor);
enableLogs(config.debug);
forwardMessage('init', null);
break;
}
case 'configure': {
self.transmuxer.configure(data.config, data.state);
break;
}
case 'demux': {
const transmuxResult: TransmuxerResult | Promise<TransmuxerResult> =
self.transmuxer.push(data.data, data.decryptdata, data.chunkMeta);
if (isPromise(transmuxResult)) {
transmuxResult.then((data) => {
emitTransmuxComplete(self, data);
});
} else {
emitTransmuxComplete(self, transmuxResult);
case 'init': {
const config = JSON.parse(data.config);
self.transmuxer = new Transmuxer(observer, data.typeSupported, config, data.vendor);
enableLogs(config.debug);
forwardMessage('init', null);
break;
}
break;
}
case 'flush': {
const id = data.chunkMeta;
const transmuxResult = self.transmuxer.flush(id);
if (isPromise(transmuxResult)) {
transmuxResult.then((results: Array<TransmuxerResult>) => {
handleFlushResult(self, results as Array<TransmuxerResult>, id);
});
} else {
handleFlushResult(self, transmuxResult as Array<TransmuxerResult>, id);
case 'configure': {
self.transmuxer.configure(data.config, data.state);
break;
}
break;
}
default:
break;
case 'demux': {
const transmuxResult: TransmuxerResult | Promise<TransmuxerResult> = self.transmuxer.push(data.data, data.decryptdata, data.chunkMeta);
if (isPromise(transmuxResult)) {
transmuxResult.then((data) => {
emitTransmuxComplete(self, data);
});
} else {
emitTransmuxComplete(self, transmuxResult);
}
break;
}
case 'flush': {
const id = data.chunkMeta;
const transmuxResult = self.transmuxer.flush(id);
if (isPromise(transmuxResult)) {
transmuxResult.then((results: Array<TransmuxerResult>) => {
handleFlushResult(self, results as Array<TransmuxerResult>, id);
});
} else {
handleFlushResult(self, transmuxResult as Array<TransmuxerResult>, id);
}
break;
}
default:
break;
}
});
}
function emitTransmuxComplete (self: any, transmuxResult: TransmuxerResult) {
function emitTransmuxComplete(self: any, transmuxResult: TransmuxerResult) {
if (isEmptyResult(transmuxResult.remuxResult)) {
return;
}
@@ -76,7 +75,7 @@ function emitTransmuxComplete (self: any, transmuxResult: TransmuxerResult) {
// Converts data to a transferable object https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast)
// in order to minimize message passing overhead
function addToTransferable (transferable: Array<ArrayBuffer>, track: RemuxedTrack) {
function addToTransferable(transferable: Array<ArrayBuffer>, track: RemuxedTrack) {
if (track.data1) {
transferable.push(track.data1.buffer);
}
@@ -85,17 +84,13 @@ function addToTransferable (transferable: Array<ArrayBuffer>, track: RemuxedTrac
}
}
function handleFlushResult (self: any, results: Array<TransmuxerResult>, chunkMeta: ChunkMetadata) {
results.forEach(result => {
function handleFlushResult(self: any, results: Array<TransmuxerResult>, chunkMeta: ChunkMetadata) {
results.forEach((result) => {
emitTransmuxComplete(self, result);
});
self.postMessage({ event: 'flush', data: chunkMeta });
}
function isEmptyResult (remuxResult: RemuxerResult) {
return !remuxResult.audio &&
!remuxResult.video &&
!remuxResult.text &&
!remuxResult.id3 &&
!remuxResult.initSegment;
function isEmptyResult(remuxResult: RemuxerResult) {
return !remuxResult.audio && !remuxResult.video && !remuxResult.text && !remuxResult.id3 && !remuxResult.initSegment;
}
+45 -42
View File
@@ -27,16 +27,16 @@ try {
}
type MuxConfig =
{ demux: typeof TSDemuxer, remux: typeof MP4Remuxer } |
{ demux: typeof MP4Demuxer, remux: typeof PassThroughRemuxer } |
{ demux: typeof AACDemuxer, remux: typeof MP4Remuxer } |
{ demux: typeof MP3Demuxer, remux: typeof MP4Remuxer };
| { demux: typeof TSDemuxer; remux: typeof MP4Remuxer }
| { demux: typeof MP4Demuxer; remux: typeof PassThroughRemuxer }
| { demux: typeof AACDemuxer; remux: typeof MP4Remuxer }
| { demux: typeof MP3Demuxer; remux: typeof MP4Remuxer };
const muxConfig: MuxConfig[] = [
{ demux: TSDemuxer, remux: MP4Remuxer },
{ demux: MP4Demuxer, remux: PassThroughRemuxer },
{ demux: AACDemuxer, remux: MP4Remuxer },
{ demux: MP3Demuxer, remux: MP4Remuxer }
{ demux: MP3Demuxer, remux: MP4Remuxer },
];
let minProbeByteLength = 1024;
@@ -58,14 +58,14 @@ export default class Transmuxer {
private currentTransmuxState!: TransmuxState;
private cache: ChunkCache = new ChunkCache();
constructor (observer: HlsEventEmitter, typeSupported, config: HlsConfig, vendor) {
constructor(observer: HlsEventEmitter, typeSupported, config: HlsConfig, vendor) {
this.observer = observer;
this.typeSupported = typeSupported;
this.config = config;
this.vendor = vendor;
}
configure (transmuxConfig: TransmuxConfig, state: TransmuxState) {
configure(transmuxConfig: TransmuxConfig, state: TransmuxState) {
this.transmuxConfig = transmuxConfig;
this.currentTransmuxState = state;
if (this.decrypter) {
@@ -73,10 +73,7 @@ export default class Transmuxer {
}
}
push (data: ArrayBuffer,
decryptdata: any | null,
chunkMeta: ChunkMetadata
): TransmuxerResult | Promise<TransmuxerResult> {
push(data: ArrayBuffer, decryptdata: any | null, chunkMeta: ChunkMetadata): TransmuxerResult | Promise<TransmuxerResult> {
const stats = chunkMeta.transmuxing;
stats.executeStart = now();
@@ -97,14 +94,15 @@ export default class Transmuxer {
}
uintData = new Uint8Array(decryptedData);
} else {
this.decryptionPromise = decrypter.webCryptoDecrypt(uintData, decryptdata.key.buffer, decryptdata.iv.buffer)
.then((decryptedData) : TransmuxerResult => {
this.decryptionPromise = decrypter.webCryptoDecrypt(uintData, decryptdata.key.buffer, decryptdata.iv.buffer).then(
(decryptedData): TransmuxerResult => {
// Calling push here is important; if flush() is called while this is still resolving, this ensures that
// the decrypted data has been transmuxed
const result = this.push(decryptedData, null, chunkMeta) as TransmuxerResult;
this.decryptionPromise = null;
return result;
});
}
);
return this.decryptionPromise!;
}
}
@@ -151,7 +149,7 @@ export default class Transmuxer {
}
// Due to data caching, flush calls can produce more than one TransmuxerResult (hence the Array type)
flush (chunkMeta: ChunkMetadata) : TransmuxerResult[] | Promise<TransmuxerResult[]> {
flush(chunkMeta: ChunkMetadata): TransmuxerResult[] | Promise<TransmuxerResult[]> {
const stats = chunkMeta.transmuxing;
stats.executeStart = now();
@@ -188,7 +186,7 @@ export default class Transmuxer {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.FRAG_PARSING_ERROR,
fatal: true,
reason: 'no demux matching with content found'
reason: 'no demux matching with content found',
});
}
stats.executeEnd = now();
@@ -200,14 +198,14 @@ export default class Transmuxer {
const remuxResult = remuxer.remux(audioTrack, avcTrack, id3Track, textTrack, timeOffset, accurateTimeOffset);
transmuxResults.push({
remuxResult,
chunkMeta
chunkMeta,
});
stats.executeEnd = now();
return transmuxResults;
}
resetInitialTimestamp (defaultInitPts: number | undefined) {
resetInitialTimestamp(defaultInitPts: number | undefined) {
const { demuxer, remuxer } = this;
if (!demuxer || !remuxer) {
return;
@@ -216,7 +214,7 @@ export default class Transmuxer {
remuxer.resetTimeStamp(defaultInitPts);
}
resetContiguity () {
resetContiguity() {
const { demuxer, remuxer } = this;
if (!demuxer || !remuxer) {
return;
@@ -225,7 +223,7 @@ export default class Transmuxer {
remuxer.resetNextTimestamp();
}
resetInitSegment (initSegmentData: Uint8Array, audioCodec: string | undefined, videoCodec: string | undefined, duration: number) {
resetInitSegment(initSegmentData: Uint8Array, audioCodec: string | undefined, videoCodec: string | undefined, duration: number) {
const { demuxer, remuxer } = this;
if (!demuxer || !remuxer) {
return;
@@ -234,7 +232,7 @@ export default class Transmuxer {
remuxer.resetInitSegment(initSegmentData, audioCodec, videoCodec);
}
destroy (): void {
destroy(): void {
if (this.demuxer) {
this.demuxer.destroy();
this.demuxer = undefined;
@@ -245,7 +243,14 @@ export default class Transmuxer {
}
}
private transmux (data: Uint8Array, decryptData: Uint8Array, encryptionType: string | null, timeOffset: number, accurateTimeOffset: boolean, chunkMeta: ChunkMetadata): TransmuxerResult | Promise<TransmuxerResult> {
private transmux(
data: Uint8Array,
decryptData: Uint8Array,
encryptionType: string | null,
timeOffset: number,
accurateTimeOffset: boolean,
chunkMeta: ChunkMetadata
): TransmuxerResult | Promise<TransmuxerResult> {
let result: TransmuxerResult | Promise<TransmuxerResult>;
if (encryptionType === 'SAMPLE-AES') {
result = this.transmuxSampleAes(data, decryptData, timeOffset, accurateTimeOffset, chunkMeta);
@@ -255,26 +260,24 @@ export default class Transmuxer {
return result;
}
private transmuxUnencrypted (data: Uint8Array, timeOffset: number, accurateTimeOffset: boolean, chunkMeta: ChunkMetadata): TransmuxerResult {
private transmuxUnencrypted(data: Uint8Array, timeOffset: number, accurateTimeOffset: boolean, chunkMeta: ChunkMetadata): TransmuxerResult {
const { audioTrack, avcTrack, id3Track, textTrack } = this.demuxer!.demux(data, timeOffset, false);
const remuxResult = this.remuxer!.remux(audioTrack, avcTrack, id3Track, textTrack, timeOffset, accurateTimeOffset);
return {
remuxResult,
chunkMeta
chunkMeta,
};
}
// TODO: Handle flush with Sample-AES
private transmuxSampleAes (data: Uint8Array, decryptData: any, timeOffset: number, accurateTimeOffset: boolean, chunkMeta: ChunkMetadata) : Promise<TransmuxerResult> {
return this.demuxer!.demuxSampleAes(data, decryptData, timeOffset)
.then(demuxResult => ({
remuxResult: this.remuxer!.remux(demuxResult.audioTrack, demuxResult.avcTrack, demuxResult.id3Track, demuxResult.textTrack, timeOffset, accurateTimeOffset),
chunkMeta
})
);
private transmuxSampleAes(data: Uint8Array, decryptData: any, timeOffset: number, accurateTimeOffset: boolean, chunkMeta: ChunkMetadata): Promise<TransmuxerResult> {
return this.demuxer!.demuxSampleAes(data, decryptData, timeOffset).then((demuxResult) => ({
remuxResult: this.remuxer!.remux(demuxResult.audioTrack, demuxResult.avcTrack, demuxResult.id3Track, demuxResult.textTrack, timeOffset, accurateTimeOffset),
chunkMeta,
}));
}
private configureTransmuxer (data: Uint8Array, transmuxConfig: TransmuxConfig): { remuxer: Remuxer | undefined, demuxer: Demuxer | undefined } {
private configureTransmuxer(data: Uint8Array, transmuxConfig: TransmuxConfig): { remuxer: Remuxer | undefined; demuxer: Demuxer | undefined } {
const { config, observer, typeSupported, vendor } = this;
const { audioCodec, defaultInitPts, duration, initSegmentData, videoCodec } = transmuxConfig;
// probe for content type
@@ -307,13 +310,13 @@ export default class Transmuxer {
return { demuxer, remuxer };
}
private needsProbing (data: Uint8Array, discontinuity: boolean, trackSwitch: boolean) : boolean {
private needsProbing(data: Uint8Array, discontinuity: boolean, trackSwitch: boolean): boolean {
// in case of continuity change, or track switch
// we might switch from content type (AAC container to TS container, or TS to fmp4 for example)
return !this.demuxer || ((discontinuity || trackSwitch));
return !this.demuxer || discontinuity || trackSwitch;
}
private getDecrypter () {
private getDecrypter() {
let decrypter = this.decrypter;
if (!decrypter) {
decrypter = this.decrypter = new Decrypter(this.observer, this.config);
@@ -322,20 +325,20 @@ export default class Transmuxer {
}
}
function getEncryptionType (data: Uint8Array, decryptData: any): string | null {
function getEncryptionType(data: Uint8Array, decryptData: any): string | null {
let encryptionType = null;
if ((data.byteLength > 0) && (decryptData != null) && (decryptData.key != null)) {
if (data.byteLength > 0 && decryptData != null && decryptData.key != null) {
encryptionType = decryptData.method;
}
return encryptionType;
}
const emptyResult = (chunkMeta) : TransmuxerResult => ({
const emptyResult = (chunkMeta): TransmuxerResult => ({
remuxResult: {},
chunkMeta
chunkMeta,
});
export function isPromise<T> (p: Promise<T> | any): p is Promise<T> {
export function isPromise<T>(p: Promise<T> | any): p is Promise<T> {
return 'then' in p && p.then instanceof Function;
}
@@ -346,7 +349,7 @@ export class TransmuxConfig {
public duration: number;
public defaultInitPts?: number;
constructor (audioCodec: string | undefined, videoCodec: string | undefined, initSegmentData: Uint8Array, duration: number, defaultInitPts?: number) {
constructor(audioCodec: string | undefined, videoCodec: string | undefined, initSegmentData: Uint8Array, duration: number, defaultInitPts?: number) {
this.audioCodec = audioCodec;
this.videoCodec = videoCodec;
this.initSegmentData = initSegmentData;
@@ -362,7 +365,7 @@ export class TransmuxState {
public trackSwitch: boolean;
public timeOffset: number;
constructor (discontinuity: boolean, contiguous: boolean, accurateTimeOffset: boolean, trackSwitch: boolean, timeOffset: number) {
constructor(discontinuity: boolean, contiguous: boolean, accurateTimeOffset: boolean, trackSwitch: boolean, timeOffset: number) {
this.discontinuity = discontinuity;
this.contiguous = contiguous;
this.accurateTimeOffset = accurateTimeOffset;
+364 -373
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -8,7 +8,7 @@ export enum ErrorTypes {
// Identifier for a mux Error (demuxing/remuxing)
MUX_ERROR = 'muxError',
// Identifier for all other errors
OTHER_ERROR = 'otherError'
OTHER_ERROR = 'otherError',
}
/**
@@ -79,5 +79,5 @@ export enum ErrorDetails {
// Identifier for an internal call to abort a loader
INTERNAL_ABORTED = 'aborted',
// Uncategorized error
UNKNOWN = 'unknown'
UNKNOWN = 'unknown',
}
+63 -62
View File
@@ -44,7 +44,8 @@ import {
AudioTracksUpdatedData,
FragLoadEmergencyAbortedData,
LiveBackBufferData,
TrackLoadingData, BufferFlushedData
TrackLoadingData,
BufferFlushedData,
} from './types/events';
/**
@@ -112,7 +113,7 @@ export enum Events {
SUBTITLE_TRACKS_CLEARED = 'hlsSubtitleTracksCleared',
// fired when an subtitle track switch occurs - data: { id : subtitle track id }
SUBTITLE_TRACK_SWITCH = 'hlsSubtitleTrackSwitch',
// fired when a subtitle track loading starts - data: { url : subtitle track URL, id : subtitle track id }
// fired when a subtitle track loading starts - data: { url : subtitle track URL, id : subtitle track id }
SUBTITLE_TRACK_LOADING = 'hlsSubtitleTrackLoading',
// fired when a subtitle track loading finishes - data: { details : levelDetails object, id : subtitle track id, stats : LoaderStats }
SUBTITLE_TRACK_LOADED = 'hlsSubtitleTrackLoaded',
@@ -165,70 +166,70 @@ export enum Events {
}
export interface HlsListeners {
[Events.MEDIA_ATTACHING]: (event: Events.MEDIA_ATTACHING, data: MediaAttachingData) => void
[Events.MEDIA_ATTACHED]: (event: Events.MEDIA_ATTACHED, data: MediaAttachedData) => void
[Events.MEDIA_DETACHING]: (event: Events.MEDIA_DETACHING) => void
[Events.MEDIA_DETACHED]: (event: Events.MEDIA_DETACHED) => void
[Events.BUFFER_RESET]: (event: Events.BUFFER_RESET) => void
[Events.BUFFER_CODECS]: (event: Events.BUFFER_CODECS, data: BufferCodecsData) => void
[Events.BUFFER_CREATED]: (event: Events.BUFFER_CREATED, data: BufferCreatedData) => void
[Events.BUFFER_APPENDING]: (event: Events.BUFFER_APPENDING, data: BufferAppendingData) => void
[Events.BUFFER_APPENDED]: (event: Events.BUFFER_APPENDED, data: BufferAppendedData) => void
[Events.BUFFER_EOS]: (event: Events.BUFFER_EOS, data: BufferEOSData) => void
[Events.BUFFER_FLUSHING]: (event: Events.BUFFER_FLUSHING, data: BufferFlushingData) => void
[Events.BUFFER_FLUSHED]: (event: Events.BUFFER_FLUSHED, data: BufferFlushedData) => void
[Events.MANIFEST_LOADING]: (event: Events.MANIFEST_LOADING, data: ManifestLoadingData) => void
[Events.MANIFEST_LOADED]: (event: Events.MANIFEST_LOADED, data: ManifestLoadedData) => void
[Events.MANIFEST_PARSED]: (event: Events.MANIFEST_PARSED, data: ManifestParsedData) => void
[Events.LEVEL_SWITCHING]: (event: Events.LEVEL_SWITCHING, data: LevelSwitchingData) => void
[Events.LEVEL_SWITCHED]: (event: Events.LEVEL_SWITCHED, data: LevelSwitchedData) => void
[Events.LEVEL_LOADING]: (event: Events.LEVEL_LOADING, data: LevelLoadingData) => void
[Events.LEVEL_LOADED]: (event: Events.LEVEL_LOADED, data: LevelLoadedData) => void
[Events.LEVEL_UPDATED]: (event: Events.LEVEL_UPDATED, data: LevelUpdatedData) => void
[Events.LEVEL_PTS_UPDATED]: (event: Events.LEVEL_PTS_UPDATED, data: LevelPTSUpdatedData) => void
[Events.LEVELS_UPDATED]: (event: Events.LEVELS_UPDATED, data: LevelsUpdatedData) => void
[Events.AUDIO_TRACKS_UPDATED]: (event: Events.AUDIO_TRACKS_UPDATED, data: AudioTracksUpdatedData) => void
[Events.AUDIO_TRACK_SWITCHING]: (event: Events.AUDIO_TRACK_SWITCHING, data: AudioTrackSwitchingData) => void
[Events.AUDIO_TRACK_SWITCHED]: (event: Events.AUDIO_TRACK_SWITCHED, data: AudioTrackSwitchedData) => void
[Events.AUDIO_TRACK_LOADING]: (event: Events.AUDIO_TRACK_LOADING, data: TrackLoadingData) => void
[Events.AUDIO_TRACK_LOADED]: (event: Events.AUDIO_TRACK_LOADED, data: AudioTrackLoadedData) => void
[Events.SUBTITLE_TRACKS_UPDATED]: (event: Events.SUBTITLE_TRACKS_UPDATED, data: SubtitleTracksUpdatedData) => void
[Events.SUBTITLE_TRACKS_CLEARED]: (event: Events.SUBTITLE_TRACKS_CLEARED) => void
[Events.SUBTITLE_TRACK_SWITCH]: (event: Events.SUBTITLE_TRACK_SWITCH, data: SubtitleTrackSwitchData) => void
[Events.SUBTITLE_TRACK_LOADING]: (event: Events.SUBTITLE_TRACK_LOADING, data: TrackLoadingData) => void
[Events.SUBTITLE_TRACK_LOADED]: (event: Events.SUBTITLE_TRACK_LOADED, data: SubtitleTrackLoadedData) => void
[Events.SUBTITLE_FRAG_PROCESSED]: (event: Events.SUBTITLE_FRAG_PROCESSED, data: SubtitleFragProcessedData) => void
[Events.CUES_PARSED]: (event: Events.CUES_PARSED, data: CuesParsedData) => void
[Events.NON_NATIVE_TEXT_TRACKS_FOUND]: (event: Events.NON_NATIVE_TEXT_TRACKS_FOUND, data: NonNativeTextTracksData) => void
[Events.INIT_PTS_FOUND]: (event: Events.INIT_PTS_FOUND, data: InitPTSFoundData) => void
[Events.FRAG_LOADING]: (event: Events.FRAG_LOADING, data: FragLoadingData) => void
[Events.MEDIA_ATTACHING]: (event: Events.MEDIA_ATTACHING, data: MediaAttachingData) => void;
[Events.MEDIA_ATTACHED]: (event: Events.MEDIA_ATTACHED, data: MediaAttachedData) => void;
[Events.MEDIA_DETACHING]: (event: Events.MEDIA_DETACHING) => void;
[Events.MEDIA_DETACHED]: (event: Events.MEDIA_DETACHED) => void;
[Events.BUFFER_RESET]: (event: Events.BUFFER_RESET) => void;
[Events.BUFFER_CODECS]: (event: Events.BUFFER_CODECS, data: BufferCodecsData) => void;
[Events.BUFFER_CREATED]: (event: Events.BUFFER_CREATED, data: BufferCreatedData) => void;
[Events.BUFFER_APPENDING]: (event: Events.BUFFER_APPENDING, data: BufferAppendingData) => void;
[Events.BUFFER_APPENDED]: (event: Events.BUFFER_APPENDED, data: BufferAppendedData) => void;
[Events.BUFFER_EOS]: (event: Events.BUFFER_EOS, data: BufferEOSData) => void;
[Events.BUFFER_FLUSHING]: (event: Events.BUFFER_FLUSHING, data: BufferFlushingData) => void;
[Events.BUFFER_FLUSHED]: (event: Events.BUFFER_FLUSHED, data: BufferFlushedData) => void;
[Events.MANIFEST_LOADING]: (event: Events.MANIFEST_LOADING, data: ManifestLoadingData) => void;
[Events.MANIFEST_LOADED]: (event: Events.MANIFEST_LOADED, data: ManifestLoadedData) => void;
[Events.MANIFEST_PARSED]: (event: Events.MANIFEST_PARSED, data: ManifestParsedData) => void;
[Events.LEVEL_SWITCHING]: (event: Events.LEVEL_SWITCHING, data: LevelSwitchingData) => void;
[Events.LEVEL_SWITCHED]: (event: Events.LEVEL_SWITCHED, data: LevelSwitchedData) => void;
[Events.LEVEL_LOADING]: (event: Events.LEVEL_LOADING, data: LevelLoadingData) => void;
[Events.LEVEL_LOADED]: (event: Events.LEVEL_LOADED, data: LevelLoadedData) => void;
[Events.LEVEL_UPDATED]: (event: Events.LEVEL_UPDATED, data: LevelUpdatedData) => void;
[Events.LEVEL_PTS_UPDATED]: (event: Events.LEVEL_PTS_UPDATED, data: LevelPTSUpdatedData) => void;
[Events.LEVELS_UPDATED]: (event: Events.LEVELS_UPDATED, data: LevelsUpdatedData) => void;
[Events.AUDIO_TRACKS_UPDATED]: (event: Events.AUDIO_TRACKS_UPDATED, data: AudioTracksUpdatedData) => void;
[Events.AUDIO_TRACK_SWITCHING]: (event: Events.AUDIO_TRACK_SWITCHING, data: AudioTrackSwitchingData) => void;
[Events.AUDIO_TRACK_SWITCHED]: (event: Events.AUDIO_TRACK_SWITCHED, data: AudioTrackSwitchedData) => void;
[Events.AUDIO_TRACK_LOADING]: (event: Events.AUDIO_TRACK_LOADING, data: TrackLoadingData) => void;
[Events.AUDIO_TRACK_LOADED]: (event: Events.AUDIO_TRACK_LOADED, data: AudioTrackLoadedData) => void;
[Events.SUBTITLE_TRACKS_UPDATED]: (event: Events.SUBTITLE_TRACKS_UPDATED, data: SubtitleTracksUpdatedData) => void;
[Events.SUBTITLE_TRACKS_CLEARED]: (event: Events.SUBTITLE_TRACKS_CLEARED) => void;
[Events.SUBTITLE_TRACK_SWITCH]: (event: Events.SUBTITLE_TRACK_SWITCH, data: SubtitleTrackSwitchData) => void;
[Events.SUBTITLE_TRACK_LOADING]: (event: Events.SUBTITLE_TRACK_LOADING, data: TrackLoadingData) => void;
[Events.SUBTITLE_TRACK_LOADED]: (event: Events.SUBTITLE_TRACK_LOADED, data: SubtitleTrackLoadedData) => void;
[Events.SUBTITLE_FRAG_PROCESSED]: (event: Events.SUBTITLE_FRAG_PROCESSED, data: SubtitleFragProcessedData) => void;
[Events.CUES_PARSED]: (event: Events.CUES_PARSED, data: CuesParsedData) => void;
[Events.NON_NATIVE_TEXT_TRACKS_FOUND]: (event: Events.NON_NATIVE_TEXT_TRACKS_FOUND, data: NonNativeTextTracksData) => void;
[Events.INIT_PTS_FOUND]: (event: Events.INIT_PTS_FOUND, data: InitPTSFoundData) => void;
[Events.FRAG_LOADING]: (event: Events.FRAG_LOADING, data: FragLoadingData) => void;
// [Events.FRAG_LOAD_PROGRESS]: TodoEventType
[Events.FRAG_LOAD_EMERGENCY_ABORTED]: (event: Events.FRAG_LOAD_EMERGENCY_ABORTED, data: FragLoadEmergencyAbortedData) => void
[Events.FRAG_LOADED]: (event: Events.FRAG_LOADED, data: FragLoadedData) => void
[Events.FRAG_DECRYPTED]: (event: Events.FRAG_DECRYPTED, data: FragDecryptedData) => void
[Events.FRAG_PARSING_INIT_SEGMENT]: (event: Events.FRAG_PARSING_INIT_SEGMENT, data: FragParsingInitSegmentData) => void
[Events.FRAG_PARSING_USERDATA]: (event: Events.FRAG_PARSING_USERDATA, data: FragParsingUserdataData) => void
[Events.FRAG_PARSING_METADATA]: (event: Events.FRAG_PARSING_METADATA, data: FragParsingMetadataData) => void
[Events.FRAG_LOAD_EMERGENCY_ABORTED]: (event: Events.FRAG_LOAD_EMERGENCY_ABORTED, data: FragLoadEmergencyAbortedData) => void;
[Events.FRAG_LOADED]: (event: Events.FRAG_LOADED, data: FragLoadedData) => void;
[Events.FRAG_DECRYPTED]: (event: Events.FRAG_DECRYPTED, data: FragDecryptedData) => void;
[Events.FRAG_PARSING_INIT_SEGMENT]: (event: Events.FRAG_PARSING_INIT_SEGMENT, data: FragParsingInitSegmentData) => void;
[Events.FRAG_PARSING_USERDATA]: (event: Events.FRAG_PARSING_USERDATA, data: FragParsingUserdataData) => void;
[Events.FRAG_PARSING_METADATA]: (event: Events.FRAG_PARSING_METADATA, data: FragParsingMetadataData) => void;
// [Events.FRAG_PARSING_DATA]: TodoEventType
[Events.FRAG_PARSED]: (event: Events.FRAG_PARSED, data: FragParsedData) => void
[Events.FRAG_BUFFERED]: (event: Events.FRAG_BUFFERED, data: FragBufferedData) => void
[Events.FRAG_CHANGED]: (event: Events.FRAG_CHANGED, data: FragChangedData) => void
[Events.FPS_DROP]: (event: Events.FPS_DROP, data: FPSDropData) => void
[Events.FPS_DROP_LEVEL_CAPPING]: (event: Events.FPS_DROP_LEVEL_CAPPING, data: FPSDropLevelCappingData) => void
[Events.ERROR]: (event: Events.ERROR, data: ErrorData) => void
[Events.DESTROYING]: (event: Events.DESTROYING) => void
[Events.KEY_LOADING]: (event: Events.KEY_LOADING, data: KeyLoadingData) => void
[Events.KEY_LOADED]: (event: Events.KEY_LOADED, data: KeyLoadedData) => void
[Events.LIVE_BACK_BUFFER_REACHED]: (event: Events.LIVE_BACK_BUFFER_REACHED, data: LiveBackBufferData) => void
[Events.FRAG_PARSED]: (event: Events.FRAG_PARSED, data: FragParsedData) => void;
[Events.FRAG_BUFFERED]: (event: Events.FRAG_BUFFERED, data: FragBufferedData) => void;
[Events.FRAG_CHANGED]: (event: Events.FRAG_CHANGED, data: FragChangedData) => void;
[Events.FPS_DROP]: (event: Events.FPS_DROP, data: FPSDropData) => void;
[Events.FPS_DROP_LEVEL_CAPPING]: (event: Events.FPS_DROP_LEVEL_CAPPING, data: FPSDropLevelCappingData) => void;
[Events.ERROR]: (event: Events.ERROR, data: ErrorData) => void;
[Events.DESTROYING]: (event: Events.DESTROYING) => void;
[Events.KEY_LOADING]: (event: Events.KEY_LOADING, data: KeyLoadingData) => void;
[Events.KEY_LOADED]: (event: Events.KEY_LOADED, data: KeyLoadedData) => void;
[Events.LIVE_BACK_BUFFER_REACHED]: (event: Events.LIVE_BACK_BUFFER_REACHED, data: LiveBackBufferData) => void;
}
export interface HlsEventEmitter {
on<E extends keyof HlsListeners, Context = undefined> (event: E, listener: HlsListeners[E], context?: Context): void
once<E extends keyof HlsListeners, Context = undefined> (event: E, listener: HlsListeners[E], context?: Context): void
on<E extends keyof HlsListeners, Context = undefined>(event: E, listener: HlsListeners[E], context?: Context): void;
once<E extends keyof HlsListeners, Context = undefined>(event: E, listener: HlsListeners[E], context?: Context): void;
removeAllListeners<E extends keyof HlsListeners> (event?: E): void
off<E extends keyof HlsListeners, Context = undefined> (event: E, listener?: HlsListeners[E], context?: Context, once?: boolean): void
removeAllListeners<E extends keyof HlsListeners>(event?: E): void;
off<E extends keyof HlsListeners, Context = undefined>(event: E, listener?: HlsListeners[E], context?: Context, once?: boolean): void;
listeners<E extends keyof HlsListeners> (event: E): HlsListeners[E][]
emit<E extends keyof HlsListeners> (event: E, name: E, eventObject: Parameters<HlsListeners[E]>[1]): boolean
listenerCount<E extends keyof HlsListeners> (event: E): number
listeners<E extends keyof HlsListeners>(event: E): HlsListeners[E][];
emit<E extends keyof HlsListeners>(event: E, name: E, eventObject: Parameters<HlsListeners[E]>[1]): boolean;
listenerCount<E extends keyof HlsListeners>(event: E): number;
}
+122 -126
View File
@@ -1,9 +1,6 @@
import * as URLToolkit from 'url-toolkit';
import {
ErrorTypes,
ErrorDetails
} from './errors';
import { ErrorTypes, ErrorDetails } from './errors';
import PlaylistLoader from './loader/playlist-loader';
import KeyLoader from './loader/key-loader';
@@ -58,27 +55,27 @@ export default class Hls implements HlsEventEmitter {
private _media: HTMLMediaElement | null = null;
private url: string | null = null;
static get version (): string {
static get version(): string {
return __VERSION__;
}
static isSupported (): boolean {
static isSupported(): boolean {
return isSupported();
}
static get Events () {
static get Events() {
return Events;
}
static get ErrorTypes () {
static get ErrorTypes() {
return ErrorTypes;
}
static get ErrorDetails () {
static get ErrorDetails() {
return ErrorDetails;
}
static get DefaultConfig (): HlsConfig {
static get DefaultConfig(): HlsConfig {
if (!Hls.defaultConfig) {
return hlsDefaultConfig;
}
@@ -89,7 +86,7 @@ export default class Hls implements HlsEventEmitter {
/**
* @type {HlsConfig}
*/
static set DefaultConfig (defaultConfig: HlsConfig) {
static set DefaultConfig(defaultConfig: HlsConfig) {
Hls.defaultConfig = defaultConfig;
}
@@ -99,8 +96,8 @@ export default class Hls implements HlsEventEmitter {
* @constructs Hls
* @param {HlsConfig} config
*/
constructor (userConfig: Partial<HlsConfig> = {}) {
const config = this.config = mergeConfig(Hls.DefaultConfig, userConfig);
constructor(userConfig: Partial<HlsConfig> = {}) {
const config = (this.config = mergeConfig(Hls.DefaultConfig, userConfig));
this.userConfig = userConfig;
enableLogs(config.debug);
@@ -111,19 +108,19 @@ export default class Hls implements HlsEventEmitter {
}
// core controllers and network loaders
const abrController = this.abrController = new config.abrController(this); // eslint-disable-line new-cap
const abrController = (this.abrController = new config.abrController(this)); // eslint-disable-line new-cap
const bufferController = new config.bufferController(this); // eslint-disable-line new-cap
const capLevelController = this.capLevelController = new config.capLevelController(this); // eslint-disable-line new-cap
const capLevelController = (this.capLevelController = new config.capLevelController(this)); // eslint-disable-line new-cap
const fpsController = new config.fpsController(this); // eslint-disable-line new-cap
const playListLoader = new PlaylistLoader(this);
const keyLoader = new KeyLoader(this);
const id3TrackController = new ID3TrackController(this);
// network controllers
const levelController = this.levelController = new LevelController(this);
const levelController = (this.levelController = new LevelController(this));
// FragmentTracker must be defined before StreamController because the order of event handling is important
const fragmentTracker = new FragmentTracker(this);
const streamController = this.streamController = new StreamController(this, fragmentTracker);
const streamController = (this.streamController = new StreamController(this, fragmentTracker));
// Level Controller initiates loading after all controllers have received MANIFEST_PARSED
levelController.onParsedComplete = () => {
@@ -137,22 +134,10 @@ export default class Hls implements HlsEventEmitter {
// fpsController uses streamController to switch when frames are being dropped
fpsController.setStreamController(streamController);
const networkControllers = [
levelController,
streamController
];
const networkControllers = [levelController, streamController];
this.networkControllers = networkControllers;
const coreComponents = [
playListLoader,
keyLoader,
abrController,
bufferController,
capLevelController,
fpsController,
id3TrackController,
fragmentTracker
];
const coreComponents = [playListLoader, keyLoader, abrController, bufferController, capLevelController, fpsController, id3TrackController, fragmentTracker];
this.audioTrackController = this.createController(config.audioTrackController, null, networkControllers);
this.createController(config.audioStreamController, fragmentTracker, networkControllers);
@@ -166,7 +151,7 @@ export default class Hls implements HlsEventEmitter {
this.coreComponents = coreComponents;
}
createController (ControllerClass, fragmentTracker, components) {
createController(ControllerClass, fragmentTracker, components) {
if (ControllerClass) {
const controllerInstance = fragmentTracker ? new ControllerClass(this, fragmentTracker) : new ControllerClass(this);
if (components) {
@@ -178,83 +163,91 @@ export default class Hls implements HlsEventEmitter {
}
// Delegate the EventEmitter through the public API of Hls.js
on<E extends keyof HlsListeners, Context = undefined> (event: E, listener: HlsListeners[E], context?: Context) {
on<E extends keyof HlsListeners, Context = undefined>(event: E, listener: HlsListeners[E], context?: Context) {
const hlsjs = this;
this._emitter.on(event, function (this: Context, ...args: unknown[]) {
if (hlsjs.config.debug) {
listener.apply(this, args);
} else {
try {
this._emitter.on(
event,
function (this: Context, ...args: unknown[]) {
if (hlsjs.config.debug) {
listener.apply(this, args);
} catch (e) {
logger.error('An internal error happened while handling event ' + event + '. Error message: "' + e.message + '". Here is a stacktrace:', e);
hlsjs.trigger(Events.ERROR, {
type: ErrorTypes.OTHER_ERROR,
details: ErrorDetails.INTERNAL_EXCEPTION,
fatal: false,
event: event,
error: e
});
} else {
try {
listener.apply(this, args);
} catch (e) {
logger.error('An internal error happened while handling event ' + event + '. Error message: "' + e.message + '". Here is a stacktrace:', e);
hlsjs.trigger(Events.ERROR, {
type: ErrorTypes.OTHER_ERROR,
details: ErrorDetails.INTERNAL_EXCEPTION,
fatal: false,
event: event,
error: e,
});
}
}
}
}, context);
},
context
);
}
once<E extends keyof HlsListeners, Context = undefined> (event: E, listener: HlsListeners[E], context?: Context) {
once<E extends keyof HlsListeners, Context = undefined>(event: E, listener: HlsListeners[E], context?: Context) {
const hlsjs = this;
this._emitter.once(event, function (this: Context, ...args: unknown[]) {
if (hlsjs.config.debug) {
listener.apply(this, args);
} else {
try {
this._emitter.once(
event,
function (this: Context, ...args: unknown[]) {
if (hlsjs.config.debug) {
listener.apply(this, args);
} catch (e) {
logger.error('An internal error happened while handling event ' + event + '. Error message: "' + e.message + '". Here is a stacktrace:', e);
hlsjs.trigger(Events.ERROR, {
type: ErrorTypes.OTHER_ERROR,
details: ErrorDetails.INTERNAL_EXCEPTION,
fatal: false,
event: event,
error: e
});
} else {
try {
listener.apply(this, args);
} catch (e) {
logger.error('An internal error happened while handling event ' + event + '. Error message: "' + e.message + '". Here is a stacktrace:', e);
hlsjs.trigger(Events.ERROR, {
type: ErrorTypes.OTHER_ERROR,
details: ErrorDetails.INTERNAL_EXCEPTION,
fatal: false,
event: event,
error: e,
});
}
}
}
}, context);
},
context
);
}
removeAllListeners<E extends keyof HlsListeners> (event?: E | undefined) {
removeAllListeners<E extends keyof HlsListeners>(event?: E | undefined) {
this._emitter.removeAllListeners(event);
}
off<E extends keyof HlsListeners, Context = undefined> (event: E, listener?: HlsListeners[E] | undefined, context?: Context, once?: boolean | undefined) {
off<E extends keyof HlsListeners, Context = undefined>(event: E, listener?: HlsListeners[E] | undefined, context?: Context, once?: boolean | undefined) {
this._emitter.off(event, listener, context, once);
}
listeners<E extends keyof HlsListeners> (event: E): HlsListeners[E][] {
listeners<E extends keyof HlsListeners>(event: E): HlsListeners[E][] {
return this._emitter.listeners(event);
}
emit<E extends keyof HlsListeners> (event: E, name: E, eventObject: Parameters<HlsListeners[E]>[1]): boolean {
emit<E extends keyof HlsListeners>(event: E, name: E, eventObject: Parameters<HlsListeners[E]>[1]): boolean {
return this._emitter.emit(event, name, eventObject);
}
trigger<E extends keyof HlsListeners> (event: E, eventObject: Parameters<HlsListeners[E]>[1]): boolean {
trigger<E extends keyof HlsListeners>(event: E, eventObject: Parameters<HlsListeners[E]>[1]): boolean {
return this._emitter.emit(event, event, eventObject);
}
listenerCount<E extends keyof HlsListeners> (event: E): number {
listenerCount<E extends keyof HlsListeners>(event: E): number {
return this._emitter.listenerCount(event);
}
/**
* Dispose of the instance
*/
destroy () {
destroy() {
logger.log('destroy');
this.trigger(Events.DESTROYING, undefined);
this.detachMedia();
this.networkControllers.forEach(component => component.destroy());
this.coreComponents.forEach(component => component.destroy());
this.networkControllers.forEach((component) => component.destroy());
this.coreComponents.forEach((component) => component.destroy());
this.url = null;
this.removeAllListeners();
this._autoLevelCapping = -1;
@@ -264,7 +257,7 @@ export default class Hls implements HlsEventEmitter {
* Attaches Hls.js to a media element
* @param {HTMLMediaElement} media
*/
attachMedia (media: HTMLMediaElement) {
attachMedia(media: HTMLMediaElement) {
logger.log('attachMedia');
this._media = media;
this.trigger(Events.MEDIA_ATTACHING, { media: media });
@@ -273,7 +266,7 @@ export default class Hls implements HlsEventEmitter {
/**
* Detach Hls.js from the media
*/
detachMedia () {
detachMedia() {
logger.log('detachMedia');
this.trigger(Events.MEDIA_DETACHING, undefined);
this._media = null;
@@ -283,7 +276,7 @@ export default class Hls implements HlsEventEmitter {
* Set the source URL. Can be relative or absolute.
* @param {string} url
*/
loadSource (url: string) {
loadSource(url: string) {
this.stopLoad();
const media = this.media;
if (media && this.url) {
@@ -304,9 +297,9 @@ export default class Hls implements HlsEventEmitter {
* @param {number} startPosition Set the start position to stream from
* @default -1 None (from earliest point)
*/
startLoad (startPosition: number = -1) {
startLoad(startPosition: number = -1) {
logger.log(`startLoad(${startPosition})`);
this.networkControllers.forEach(controller => {
this.networkControllers.forEach((controller) => {
controller.startLoad(startPosition);
});
}
@@ -314,9 +307,9 @@ export default class Hls implements HlsEventEmitter {
/**
* Stop loading of any stream data.
*/
stopLoad () {
stopLoad() {
logger.log('stopLoad');
this.networkControllers.forEach(controller => {
this.networkControllers.forEach((controller) => {
controller.stopLoad();
});
}
@@ -324,7 +317,7 @@ export default class Hls implements HlsEventEmitter {
/**
* Swap through possible audio codecs in the stream (for example to switch from stereo to 5.1)
*/
swapAudioCodec () {
swapAudioCodec() {
logger.log('swapAudioCodec');
this.streamController.swapAudioCodec();
}
@@ -335,7 +328,7 @@ export default class Hls implements HlsEventEmitter {
*
* Automatic recovery of media-errors by this process is configurable.
*/
recoverMediaError () {
recoverMediaError() {
logger.log('recoverMediaError');
const media = this._media;
this.detachMedia();
@@ -344,14 +337,14 @@ export default class Hls implements HlsEventEmitter {
}
}
removeLevel (levelIndex, urlId = 0) {
removeLevel(levelIndex, urlId = 0) {
this.levelController.removeLevel(levelIndex, urlId);
}
/**
* @type {Level[]}
*/
get levels (): Array<Level> {
get levels(): Array<Level> {
return this.levelController.levels ? this.levelController.levels : [];
}
@@ -359,7 +352,7 @@ export default class Hls implements HlsEventEmitter {
* Index of quality level currently played
* @type {number}
*/
get currentLevel (): number {
get currentLevel(): number {
return this.streamController.currentLevel;
}
@@ -369,7 +362,7 @@ export default class Hls implements HlsEventEmitter {
* That means playback will interrupt at least shortly to re-buffer and re-sync eventually.
* @type {number} -1 for automatic level selection
*/
set currentLevel (newLevel: number) {
set currentLevel(newLevel: number) {
logger.log(`set currentLevel:${newLevel}`);
this.loadLevel = newLevel;
this.streamController.immediateLevelSwitch();
@@ -379,7 +372,7 @@ export default class Hls implements HlsEventEmitter {
* Index of next quality level loaded as scheduled by stream controller.
* @type {number}
*/
get nextLevel (): number {
get nextLevel(): number {
return this.streamController.nextLevel;
}
@@ -389,7 +382,7 @@ export default class Hls implements HlsEventEmitter {
* May abort current loading of data, and flush parts of buffer (outside currently played fragment region).
* @type {number} -1 for automatic level selection
*/
set nextLevel (newLevel: number) {
set nextLevel(newLevel: number) {
logger.log(`set nextLevel:${newLevel}`);
this.levelController.manualLevel = newLevel;
this.streamController.nextLevelSwitch();
@@ -399,7 +392,7 @@ export default class Hls implements HlsEventEmitter {
* Return the quality level of the currently or last (of none is loaded currently) segment
* @type {number}
*/
get loadLevel (): number {
get loadLevel(): number {
return this.levelController.level;
}
@@ -409,7 +402,7 @@ export default class Hls implements HlsEventEmitter {
* Thus the moment when the quality switch will appear in effect will only be after the already existing buffer.
* @type {number} newLevel -1 for automatic level selection
*/
set loadLevel (newLevel: number) {
set loadLevel(newLevel: number) {
logger.log(`set loadLevel:${newLevel}`);
this.levelController.manualLevel = newLevel;
}
@@ -418,7 +411,7 @@ export default class Hls implements HlsEventEmitter {
* get next quality level loaded
* @type {number}
*/
get nextLoadLevel (): number {
get nextLoadLevel(): number {
return this.levelController.nextLoadLevel;
}
@@ -427,7 +420,7 @@ export default class Hls implements HlsEventEmitter {
* Same as `loadLevel` but will wait for next switch (until current loading is done).
* @type {number} level
*/
set nextLoadLevel (level: number) {
set nextLoadLevel(level: number) {
this.levelController.nextLoadLevel = level;
}
@@ -436,7 +429,7 @@ export default class Hls implements HlsEventEmitter {
* falls back to index of first level referenced in manifest
* @type {number}
*/
get firstLevel (): number {
get firstLevel(): number {
return Math.max(this.levelController.firstLevel, this.minAutoLevel);
}
@@ -444,7 +437,7 @@ export default class Hls implements HlsEventEmitter {
* Sets "first-level", see getter.
* @type {number}
*/
set firstLevel (newLevel: number) {
set firstLevel(newLevel: number) {
logger.log(`set firstLevel:${newLevel}`);
this.levelController.firstLevel = newLevel;
}
@@ -456,7 +449,7 @@ export default class Hls implements HlsEventEmitter {
* (determined from download of first segment)
* @type {number}
*/
get startLevel (): number {
get startLevel(): number {
return this.levelController.startLevel;
}
@@ -467,7 +460,7 @@ export default class Hls implements HlsEventEmitter {
* (determined from download of first segment)
* @type {number} newLevel
*/
set startLevel (newLevel: number) {
set startLevel(newLevel: number) {
logger.log(`set startLevel:${newLevel}`);
// if not in automatic start level detection, ensure startLevel is greater than minAutoLevel
if (newLevel !== -1) {
@@ -482,7 +475,7 @@ export default class Hls implements HlsEventEmitter {
*
* @type {boolean}
*/
get capLevelToPlayerSize (): boolean {
get capLevelToPlayerSize(): boolean {
return this.config.capLevelToPlayerSize;
}
@@ -491,7 +484,7 @@ export default class Hls implements HlsEventEmitter {
*
* @type {boolean}
*/
set capLevelToPlayerSize (shouldStartCapping: boolean) {
set capLevelToPlayerSize(shouldStartCapping: boolean) {
const newCapLevelToPlayerSize = !!shouldStartCapping;
if (newCapLevelToPlayerSize !== this.config.capLevelToPlayerSize) {
@@ -511,7 +504,7 @@ export default class Hls implements HlsEventEmitter {
* Capping/max level value that should be used by automatic level selection algorithm (`ABRController`)
* @type {number}
*/
get autoLevelCapping (): number {
get autoLevelCapping(): number {
return this._autoLevelCapping;
}
@@ -519,7 +512,7 @@ export default class Hls implements HlsEventEmitter {
* get bandwidth estimate
* @type {number}
*/
get bandwidthEstimate (): number {
get bandwidthEstimate(): number {
return this.abrController.bwEstimator.getEstimate();
}
@@ -527,7 +520,7 @@ export default class Hls implements HlsEventEmitter {
* Capping/max level value that should be used by automatic level selection algorithm (`ABRController`)
* @type {number}
*/
set autoLevelCapping (newLevel: number) {
set autoLevelCapping(newLevel: number) {
if (this._autoLevelCapping !== newLevel) {
logger.log(`set autoLevelCapping:${newLevel}`);
this._autoLevelCapping = newLevel;
@@ -538,15 +531,15 @@ export default class Hls implements HlsEventEmitter {
* True when automatic level selection enabled
* @type {boolean}
*/
get autoLevelEnabled (): boolean {
return (this.levelController.manualLevel === -1);
get autoLevelEnabled(): boolean {
return this.levelController.manualLevel === -1;
}
/**
* Level set manually (if any)
* @type {number}
*/
get manualLevel (): number {
get manualLevel(): number {
return this.levelController.manualLevel;
}
@@ -554,8 +547,11 @@ export default class Hls implements HlsEventEmitter {
* min level selectable in auto mode according to config.minAutoBitrate
* @type {number}
*/
get minAutoLevel (): number {
const { levels, config: { minAutoBitrate } } = this;
get minAutoLevel(): number {
const {
levels,
config: { minAutoBitrate },
} = this;
if (!levels) return 0;
const len = levels.length;
@@ -572,7 +568,7 @@ export default class Hls implements HlsEventEmitter {
* max level selectable in auto mode according to autoLevelCapping
* @type {number}
*/
get maxAutoLevel (): number {
get maxAutoLevel(): number {
const { levels, autoLevelCapping } = this;
let maxAutoLevel;
@@ -589,7 +585,7 @@ export default class Hls implements HlsEventEmitter {
* next automatically selected quality level
* @type {number}
*/
get nextAutoLevel (): number {
get nextAutoLevel(): number {
// ensure next auto level is between min and max auto level
return Math.min(Math.max(this.abrController.nextAutoLevel, this.minAutoLevel), this.maxAutoLevel);
}
@@ -602,14 +598,14 @@ export default class Hls implements HlsEventEmitter {
* this value will be resetted to -1 by ABR controller.
* @type {number}
*/
set nextAutoLevel (nextLevel: number) {
set nextAutoLevel(nextLevel: number) {
this.abrController.nextAutoLevel = Math.max(this.minAutoLevel, nextLevel);
}
/**
* @type {AudioTrack[]}
*/
get audioTracks (): Array<MediaPlaylist> {
get audioTracks(): Array<MediaPlaylist> {
const audioTrackController = this.audioTrackController;
return audioTrackController ? audioTrackController.audioTracks : [];
}
@@ -618,7 +614,7 @@ export default class Hls implements HlsEventEmitter {
* index of the selected audio track (index in audio track lists)
* @type {number}
*/
get audioTrack (): number {
get audioTrack(): number {
const audioTrackController = this.audioTrackController;
return audioTrackController ? audioTrackController.audioTrack : -1;
}
@@ -627,7 +623,7 @@ export default class Hls implements HlsEventEmitter {
* selects an audio track, based on its index in audio track lists
* @type {number}
*/
set audioTrack (audioTrackId: number) {
set audioTrack(audioTrackId: number) {
const audioTrackController = this.audioTrackController;
if (audioTrackController) {
audioTrackController.audioTrack = audioTrackId;
@@ -638,7 +634,7 @@ export default class Hls implements HlsEventEmitter {
* get alternate subtitle tracks list from playlist
* @type {MediaPlaylist[]}
*/
get subtitleTracks (): Array<MediaPlaylist> {
get subtitleTracks(): Array<MediaPlaylist> {
const subtitleTrackController = this.subtitleTrackController;
return subtitleTrackController ? subtitleTrackController.subtitleTracks : [];
}
@@ -647,12 +643,12 @@ export default class Hls implements HlsEventEmitter {
* index of the selected subtitle track (index in subtitle track lists)
* @type {number}
*/
get subtitleTrack (): number {
get subtitleTrack(): number {
const subtitleTrackController = this.subtitleTrackController;
return subtitleTrackController ? subtitleTrackController.subtitleTrack : -1;
}
get media () {
get media() {
return this._media;
}
@@ -660,7 +656,7 @@ export default class Hls implements HlsEventEmitter {
* select an subtitle track, based on its index in subtitle track lists
* @type {number}
*/
set subtitleTrack (subtitleTrackId: number) {
set subtitleTrack(subtitleTrackId: number) {
const subtitleTrackController = this.subtitleTrackController;
if (subtitleTrackController) {
subtitleTrackController.subtitleTrack = subtitleTrackId;
@@ -670,7 +666,7 @@ export default class Hls implements HlsEventEmitter {
/**
* @type {boolean}
*/
get subtitleDisplay (): boolean {
get subtitleDisplay(): boolean {
const subtitleTrackController = this.subtitleTrackController;
return subtitleTrackController ? subtitleTrackController.subtitleDisplay : false;
}
@@ -679,7 +675,7 @@ export default class Hls implements HlsEventEmitter {
* Enable/disable subtitle display rendering
* @type {boolean}
*/
set subtitleDisplay (value: boolean) {
set subtitleDisplay(value: boolean) {
const subtitleTrackController = this.subtitleTrackController;
if (subtitleTrackController) {
subtitleTrackController.subtitleDisplay = value;
@@ -690,7 +686,7 @@ export default class Hls implements HlsEventEmitter {
* get mode for Low-Latency HLS loading
* @type {boolean}
*/
get lowLatencyMode () {
get lowLatencyMode() {
return this.config.lowLatencyMode;
}
@@ -698,7 +694,7 @@ export default class Hls implements HlsEventEmitter {
* Enable/disable Low-Latency HLS part playlist and segment loading, and start live streams at playlist PART-HOLD-BACK rather than HOLD-BACK.
* @type {boolean}
*/
set lowLatencyMode (mode: boolean) {
set lowLatencyMode(mode: boolean) {
this.config.lowLatencyMode = mode;
}
@@ -706,7 +702,7 @@ export default class Hls implements HlsEventEmitter {
* position (in seconds) of live sync point (ie edge of live position minus safety delay defined by ```hls.config.liveSyncDuration```)
* @type {number}
*/
get liveSyncPosition (): number | null {
get liveSyncPosition(): number | null {
return this.latencyController.liveSyncPosition;
}
@@ -715,7 +711,7 @@ export default class Hls implements HlsEventEmitter {
* returns 0 before first playlist is loaded
* @type {number}
*/
get latency () {
get latency() {
return this.latencyController.latency;
}
@@ -725,7 +721,7 @@ export default class Hls implements HlsEventEmitter {
* returns 0 before first playlist is loaded
* @type {number}
*/
get maxLatency (): number {
get maxLatency(): number {
return this.latencyController.maxLatency;
}
@@ -733,7 +729,7 @@ export default class Hls implements HlsEventEmitter {
* target distance from the edge as calculated by the latency controller
* @type {number}
*/
get targetLatency (): number | null {
get targetLatency(): number | null {
return this.latencyController.targetLatency;
}
}
+5 -10
View File
@@ -1,22 +1,17 @@
import { getMediaSource } from './utils/mediasource-helper';
export function isSupported (): boolean {
export function isSupported(): boolean {
const mediaSource = getMediaSource();
if (!mediaSource) {
return false;
}
const sourceBuffer = self.SourceBuffer ||
(self as any).WebKitSourceBuffer as SourceBuffer; // eslint-disable-line no-restricted-globals
const sourceBuffer = self.SourceBuffer || ((self as any).WebKitSourceBuffer as SourceBuffer); // eslint-disable-line no-restricted-globals
const isTypeSupported = mediaSource &&
typeof mediaSource.isTypeSupported === 'function' &&
mediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');
const isTypeSupported = mediaSource && typeof mediaSource.isTypeSupported === 'function' && mediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');
// if SourceBuffer is exposed ensure its API is valid
// safari and old version of Chrome doe not expose SourceBuffer globally so checking SourceBuffer.prototype is impossible
const sourceBufferValidAPI = !sourceBuffer ||
(sourceBuffer.prototype &&
typeof sourceBuffer.prototype.appendBuffer === 'function' &&
typeof sourceBuffer.prototype.remove === 'function');
const sourceBufferValidAPI =
!sourceBuffer || (sourceBuffer.prototype && typeof sourceBuffer.prototype.appendBuffer === 'function' && typeof sourceBuffer.prototype.remove === 'function');
return !!isTypeSupported && !!sourceBufferValidAPI;
}
+99 -88
View File
@@ -1,10 +1,6 @@
import { ErrorTypes, ErrorDetails } from '../errors';
import Fragment from './fragment';
import {
Loader,
LoaderConfiguration,
FragmentLoaderContext
} from '../types/loader';
import { Loader, LoaderConfiguration, FragmentLoaderContext } from '../types/loader';
import type { HlsConfig } from '../config';
import type { BaseSegment, Part } from './fragment';
import type { FragLoadedData } from '../types/events';
@@ -16,27 +12,32 @@ export default class FragmentLoader {
private loader: Loader<FragmentLoaderContext> | null = null;
private partLoadTimeout: number = -1;
constructor (config: HlsConfig) {
constructor(config: HlsConfig) {
this.config = config;
}
abort () {
abort() {
if (this.loader) {
// Abort the loader for current fragment. Only one may load at any given time
this.loader.abort();
}
}
load (frag: Fragment, onProgress?: FragmentLoadProgressCallback): Promise<FragLoadedData> {
load(frag: Fragment, onProgress?: FragmentLoadProgressCallback): Promise<FragLoadedData> {
const url = frag.url;
if (!url) {
return Promise.reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_ERROR,
fatal: false,
frag,
networkDetails: null
}, `Fragment does not have a ${url ? 'part list' : 'url'}`));
return Promise.reject(
new LoadError(
{
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_ERROR,
fatal: false,
frag,
networkDetails: null,
},
`Fragment does not have a ${url ? 'part list' : 'url'}`
)
);
}
this.abort();
@@ -45,15 +46,14 @@ export default class FragmentLoader {
const DefaultILoader = config.loader;
return new Promise((resolve, reject) => {
const loader = this.loader = frag.loader =
FragmentILoader ? new FragmentILoader(config) : new DefaultILoader(config) as Loader<FragmentLoaderContext>;
const loader = (this.loader = frag.loader = FragmentILoader ? new FragmentILoader(config) : (new DefaultILoader(config) as Loader<FragmentLoaderContext>));
const loaderContext = createLoaderContext(frag);
const loaderConfig: LoaderConfiguration = {
timeout: config.fragLoadingTimeOut,
maxRetry: 0,
retryDelay: 0,
maxRetryDelay: config.fragLoadingMaxRetryTimeout,
highWaterMark: MIN_CHUNK_SIZE
highWaterMark: MIN_CHUNK_SIZE,
};
// Assign frag stats to the loader's stats reference
frag.stats = loader.stats;
@@ -64,39 +64,45 @@ export default class FragmentLoader {
frag,
part: null,
payload: response.data as ArrayBuffer,
networkDetails
networkDetails,
});
},
onError: (response, context, networkDetails) => {
this.resetLoader(frag, loader);
reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_ERROR,
fatal: false,
frag,
response,
networkDetails
}));
reject(
new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_ERROR,
fatal: false,
frag,
response,
networkDetails,
})
);
},
onAbort: (stats, context, networkDetails) => {
this.resetLoader(frag, loader);
reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.INTERNAL_ABORTED,
fatal: false,
frag,
networkDetails
}));
reject(
new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.INTERNAL_ABORTED,
fatal: false,
frag,
networkDetails,
})
);
},
onTimeout: (response, context, networkDetails) => {
this.resetLoader(frag, loader);
reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_TIMEOUT,
fatal: false,
frag,
networkDetails
}));
reject(
new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_TIMEOUT,
fatal: false,
frag,
networkDetails,
})
);
},
onProgress: (stats, context, data, networkDetails) => {
if (onProgress) {
@@ -104,15 +110,15 @@ export default class FragmentLoader {
frag,
part: null,
payload: data as ArrayBuffer,
networkDetails
networkDetails,
});
}
}
},
});
});
}
public loadPart (frag: Fragment, part: Part, onProgress: FragmentLoadProgressCallback): Promise<FragLoadedData> {
public loadPart(frag: Fragment, part: Part, onProgress: FragmentLoadProgressCallback): Promise<FragLoadedData> {
this.abort();
const config = this.config;
@@ -120,15 +126,14 @@ export default class FragmentLoader {
const DefaultILoader = config.loader;
return new Promise((resolve, reject) => {
const loader = this.loader = frag.loader =
FragmentILoader ? new FragmentILoader(config) : new DefaultILoader(config) as Loader<FragmentLoaderContext>;
const loader = (this.loader = frag.loader = FragmentILoader ? new FragmentILoader(config) : (new DefaultILoader(config) as Loader<FragmentLoaderContext>));
const loaderContext = createLoaderContext(frag, part);
const loaderConfig: LoaderConfiguration = {
timeout: config.fragLoadingTimeOut,
maxRetry: 0,
retryDelay: 0,
maxRetryDelay: config.fragLoadingMaxRetryTimeout,
highWaterMark: MIN_CHUNK_SIZE
highWaterMark: MIN_CHUNK_SIZE,
};
// Assign part stats to the loader's stats reference
part.stats = loader.stats;
@@ -140,51 +145,57 @@ export default class FragmentLoader {
frag,
part,
payload: response.data as ArrayBuffer,
networkDetails
networkDetails,
};
onProgress(partLoadedData);
resolve(partLoadedData);
},
onError: (response, context, networkDetails) => {
this.resetLoader(frag, loader);
reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_ERROR,
fatal: false,
frag,
part,
response,
networkDetails
}));
reject(
new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_ERROR,
fatal: false,
frag,
part,
response,
networkDetails,
})
);
},
onAbort: (stats, context, networkDetails) => {
frag.stats.aborted = part.stats.aborted;
this.resetLoader(frag, loader);
reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.INTERNAL_ABORTED,
fatal: false,
frag,
part,
networkDetails
}));
reject(
new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.INTERNAL_ABORTED,
fatal: false,
frag,
part,
networkDetails,
})
);
},
onTimeout: (response, context, networkDetails) => {
this.resetLoader(frag, loader);
reject(new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_TIMEOUT,
fatal: false,
frag,
part,
networkDetails
}));
}
reject(
new LoadError({
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.FRAG_LOAD_TIMEOUT,
fatal: false,
frag,
part,
networkDetails,
})
);
},
});
});
}
private updateStatsFromPart (frag: Fragment, part: Part) {
private updateStatsFromPart(frag: Fragment, part: Part) {
const fragStats = frag.stats;
const partStats = part.stats;
const partTotal = partStats.total;
@@ -210,7 +221,7 @@ export default class FragmentLoader {
fragLoading.end = partLoading.end;
}
private resetLoader (frag: Fragment, loader: Loader<FragmentLoaderContext>) {
private resetLoader(frag: Fragment, loader: Loader<FragmentLoaderContext>) {
frag.loader = null;
if (this.loader === loader) {
self.clearTimeout(this.partLoadTimeout);
@@ -219,7 +230,7 @@ export default class FragmentLoader {
}
}
function createLoaderContext (frag: Fragment, part: Part | null = null): FragmentLoaderContext {
function createLoaderContext(frag: Fragment, part: Part | null = null): FragmentLoaderContext {
const segment: BaseSegment = part || frag;
const loaderContext: FragmentLoaderContext = {
frag,
@@ -227,7 +238,7 @@ function createLoaderContext (frag: Fragment, part: Part | null = null): Fragmen
responseType: 'arraybuffer',
url: segment.url,
rangeStart: 0,
rangeEnd: 0
rangeEnd: 0,
};
const start = segment.byteRangeStartOffset;
const end = segment.byteRangeEndOffset;
@@ -240,25 +251,25 @@ function createLoaderContext (frag: Fragment, part: Part | null = null): Fragmen
export class LoadError extends Error {
public readonly data: FragLoadFailResult;
constructor (data: FragLoadFailResult, ...params) {
constructor(data: FragLoadFailResult, ...params) {
super(...params);
this.data = data;
}
}
export interface FragLoadFailResult {
type: string
details: string
fatal: boolean
frag: Fragment
part?: Part
type: string;
details: string;
fatal: boolean;
frag: Fragment;
part?: Part;
response?: {
// error status code
code: number,
code: number;
// error description
text: string,
}
networkDetails: any
text: string;
};
networkDetails: any;
}
export type FragmentLoadProgressCallback = (result: FragLoadedData) => void;
+30 -30
View File
@@ -8,15 +8,15 @@ import type { FragmentLoaderContext, Loader, PlaylistLevelType } from '../types/
export enum ElementaryStreamTypes {
AUDIO = 'audio',
VIDEO = 'video',
AUDIOVIDEO = 'audiovideo'
AUDIOVIDEO = 'audiovideo',
}
interface ElementaryStreamInfo {
startPTS: number
endPTS: number
startDTS: number
endDTS: number
partial?: boolean
startPTS: number;
endPTS: number;
startDTS: number;
endDTS: number;
partial?: boolean;
}
type ElementaryStreams = Record<ElementaryStreamTypes, ElementaryStreamInfo | null>;
@@ -33,15 +33,15 @@ export class BaseSegment {
public elementaryStreams: ElementaryStreams = {
[ElementaryStreamTypes.AUDIO]: null,
[ElementaryStreamTypes.VIDEO]: null,
[ElementaryStreamTypes.AUDIOVIDEO]: null
[ElementaryStreamTypes.AUDIOVIDEO]: null,
};
constructor (baseurl: string) {
constructor(baseurl: string) {
this.baseurl = baseurl;
}
// setByteRange converts a EXT-X-BYTERANGE attribute into a two element array
setByteRange (value: string, previous?: BaseSegment) {
setByteRange(value: string, previous?: BaseSegment) {
const params = value.split('@', 2);
const byteRange: number[] = [];
if (params.length === 1) {
@@ -53,7 +53,7 @@ export class BaseSegment {
this._byteRange = byteRange;
}
get byteRange (): number[] {
get byteRange(): number[] {
if (!this._byteRange) {
return [];
}
@@ -61,22 +61,22 @@ export class BaseSegment {
return this._byteRange;
}
get byteRangeStartOffset (): number {
get byteRangeStartOffset(): number {
return this.byteRange[0];
}
get byteRangeEndOffset (): number {
get byteRangeEndOffset(): number {
return this.byteRange[1];
}
get url (): string {
get url(): string {
if (!this._url && this.baseurl && this.relurl) {
this._url = buildAbsoluteURL(this.baseurl, this.relurl, { alwaysNormalize: true });
}
return this._url || '';
}
set url (value: string) {
set url(value: string) {
this._url = value;
}
}
@@ -131,12 +131,12 @@ export default class Fragment extends BaseSegment {
// #EXTINF segment title
public title: string | null = null;
constructor (type: PlaylistLevelType, baseurl: string) {
constructor(type: PlaylistLevelType, baseurl: string) {
super(baseurl);
this.type = type;
}
get decryptdata (): LevelKey | null {
get decryptdata(): LevelKey | null {
if (!this.levelkey && !this._decryptdata) {
return null;
}
@@ -166,11 +166,11 @@ export default class Fragment extends BaseSegment {
return this._decryptdata;
}
get end (): number {
get end(): number {
return this.start + this.duration;
}
get endProgramDateTime () {
get endProgramDateTime() {
if (this.programDateTime === null) {
return null;
}
@@ -181,10 +181,10 @@ export default class Fragment extends BaseSegment {
const duration = !Number.isFinite(this.duration) ? 0 : this.duration;
return this.programDateTime + (duration * 1000);
return this.programDateTime + duration * 1000;
}
get encrypted () {
get encrypted() {
// At the m3u8-parser level we need to add support for manifest signalled keyformats
// when we want the fragment to start reporting that it is encrypted.
// Currently, keyFormat will only be set for identity keys
@@ -200,11 +200,11 @@ export default class Fragment extends BaseSegment {
* @param {number} segmentNumber - segment number to generate IV with
* @returns {Uint8Array}
*/
createInitializationVector (segmentNumber: number): Uint8Array {
createInitializationVector(segmentNumber: number): Uint8Array {
const uint8View = new Uint8Array(16);
for (let i = 12; i < 16; i++) {
uint8View[i] = (segmentNumber >> 8 * (15 - i)) & 0xff;
uint8View[i] = (segmentNumber >> (8 * (15 - i))) & 0xff;
}
return uint8View;
@@ -216,7 +216,7 @@ export default class Fragment extends BaseSegment {
* @param segmentNumber - the fragment's segment number
* @returns {LevelKey} - an object to be applied as a fragment's decryptdata
*/
setDecryptDataFromLevelKey (levelkey: LevelKey, segmentNumber: number): LevelKey {
setDecryptDataFromLevelKey(levelkey: LevelKey, segmentNumber: number): LevelKey {
let decryptdata = levelkey;
if (levelkey?.method === 'AES-128' && levelkey.uri && !levelkey.iv) {
@@ -229,7 +229,7 @@ export default class Fragment extends BaseSegment {
return decryptdata;
}
setElementaryStreamInfo (type: ElementaryStreamTypes, startPTS: number, endPTS: number, startDTS: number, endDTS: number, partial: boolean = false) {
setElementaryStreamInfo(type: ElementaryStreamTypes, startPTS: number, endPTS: number, startDTS: number, endDTS: number, partial: boolean = false) {
const { elementaryStreams } = this;
const info = elementaryStreams[type];
if (!info) {
@@ -238,7 +238,7 @@ export default class Fragment extends BaseSegment {
endPTS,
startDTS,
endDTS,
partial
partial,
};
return;
}
@@ -249,7 +249,7 @@ export default class Fragment extends BaseSegment {
info.endDTS = Math.max(info.endDTS, endDTS);
}
clearElementaryStreamInfo () {
clearElementaryStreamInfo() {
const { elementaryStreams } = this;
elementaryStreams[ElementaryStreamTypes.AUDIO] = null;
elementaryStreams[ElementaryStreamTypes.VIDEO] = null;
@@ -267,7 +267,7 @@ export class Part extends BaseSegment {
public readonly index: number;
public stats: LoadStats = new LoadStats();
constructor (partAttrs: AttrList, frag: Fragment, baseurl: string, index: number, previous?: Part) {
constructor(partAttrs: AttrList, frag: Fragment, baseurl: string, index: number, previous?: Part) {
super(baseurl);
this.duration = partAttrs.decimalFloatingPoint('DURATION');
this.gap = partAttrs.bool('GAP');
@@ -284,15 +284,15 @@ export class Part extends BaseSegment {
}
}
get start (): number {
get start(): number {
return this.fragment.start + this.fragOffset;
}
get end (): number {
get end(): number {
return this.start + this.duration;
}
get loaded (): boolean {
get loaded(): boolean {
const { elementaryStreams } = this;
return !!(elementaryStreams.audio || elementaryStreams.video || elementaryStreams.audiovideo);
}
+15 -22
View File
@@ -1,24 +1,17 @@
/*
* Decrypt key Loader
*/
*/
import { Events } from '../events';
import { ErrorTypes, ErrorDetails } from '../errors';
import { logger } from '../utils/logger';
import type Hls from '../hls';
import Fragment from './fragment';
import {
LoaderStats,
LoaderResponse,
LoaderContext,
LoaderConfiguration,
LoaderCallbacks,
Loader, FragmentLoaderContext
} from '../types/loader';
import { LoaderStats, LoaderResponse, LoaderContext, LoaderConfiguration, LoaderCallbacks, Loader, FragmentLoaderContext } from '../types/loader';
import type { ComponentAPI } from '../types/component-api';
import type { KeyLoadingData } from '../types/events';
interface KeyLoaderContext extends LoaderContext {
frag: Fragment
frag: Fragment;
}
export default class KeyLoader implements ComponentAPI {
@@ -27,21 +20,21 @@ export default class KeyLoader implements ComponentAPI {
public decryptkey: Uint8Array | null = null;
public decrypturl: string | null = null;
constructor (hls: Hls) {
constructor(hls: Hls) {
this.hls = hls;
this._registerListeners();
}
private _registerListeners () {
private _registerListeners() {
this.hls.on(Events.KEY_LOADING, this.onKeyLoading, this);
}
private _unregisterListeners () {
private _unregisterListeners() {
this.hls.off(Events.KEY_LOADING, this.onKeyLoading);
}
destroy (): void {
destroy(): void {
this._unregisterListeners();
for (const loaderName in this.loaders) {
const loader = this.loaders[loaderName];
@@ -52,7 +45,7 @@ export default class KeyLoader implements ComponentAPI {
this.loaders = {};
}
onKeyLoading (event: Events.KEY_LOADING, data: KeyLoadingData) {
onKeyLoading(event: Events.KEY_LOADING, data: KeyLoadingData) {
const { frag } = data;
const type = frag.type;
const loader = this.loaders[type];
@@ -74,14 +67,14 @@ export default class KeyLoader implements ComponentAPI {
return;
}
const Loader = config.loader;
const fragLoader = frag.loader = this.loaders[type] = new Loader(config) as Loader<FragmentLoaderContext>;
const fragLoader = (frag.loader = this.loaders[type] = new Loader(config) as Loader<FragmentLoaderContext>);
this.decrypturl = uri;
this.decryptkey = null;
const loaderContext: KeyLoaderContext = {
url: uri,
frag: frag,
responseType: 'arraybuffer'
responseType: 'arraybuffer',
};
// maxRetry is 0 so that instead of retrying the same key on the same variant multiple times,
@@ -92,13 +85,13 @@ export default class KeyLoader implements ComponentAPI {
maxRetry: 0,
retryDelay: config.fragLoadingRetryDelay,
maxRetryDelay: config.fragLoadingMaxRetryTimeout,
highWaterMark: 0
highWaterMark: 0,
};
const loaderCallbacks: LoaderCallbacks<KeyLoaderContext> = {
onSuccess: this.loadsuccess.bind(this),
onError: this.loaderror.bind(this),
onTimeout: this.loadtimeout.bind(this)
onTimeout: this.loadtimeout.bind(this),
};
fragLoader.load(loaderContext, loaderConfig, loaderCallbacks);
@@ -109,7 +102,7 @@ export default class KeyLoader implements ComponentAPI {
}
}
loadsuccess (response: LoaderResponse, stats: LoaderStats, context: KeyLoaderContext) {
loadsuccess(response: LoaderResponse, stats: LoaderStats, context: KeyLoaderContext) {
const frag = context.frag;
if (!frag.decryptdata) {
logger.error('after key load, decryptdata unset');
@@ -123,7 +116,7 @@ export default class KeyLoader implements ComponentAPI {
this.hls.trigger(Events.KEY_LOADED, { frag: frag });
}
loaderror (response: LoaderResponse, context: KeyLoaderContext) {
loaderror(response: LoaderResponse, context: KeyLoaderContext) {
const frag = context.frag;
const loader = frag.loader;
if (loader) {
@@ -134,7 +127,7 @@ export default class KeyLoader implements ComponentAPI {
this.hls.trigger(Events.ERROR, { type: ErrorTypes.NETWORK_ERROR, details: ErrorDetails.KEY_LOAD_ERROR, fatal: false, frag, response });
}
loadtimeout (stats: LoaderStats, context: KeyLoaderContext) {
loadtimeout(stats: LoaderStats, context: KeyLoaderContext) {
const frag = context.frag;
const loader = frag.loader;
if (loader) {
+10 -10
View File
@@ -43,12 +43,12 @@ export default class LevelDetails {
public tuneInGoal: number = 0;
public deltaUpdateFailed?: boolean;
constructor (baseUrl) {
constructor(baseUrl) {
this.fragments = [];
this.url = baseUrl;
}
reloaded (previous: LevelDetails | undefined) {
reloaded(previous: LevelDetails | undefined) {
if (!previous) {
this.advanced = true;
this.updated = true;
@@ -66,50 +66,50 @@ export default class LevelDetails {
this.availabilityDelay = previous.availabilityDelay;
}
get hasProgramDateTime (): boolean {
get hasProgramDateTime(): boolean {
if (this.fragments.length) {
return Number.isFinite(this.fragments[this.fragments.length - 1].programDateTime as number);
}
return false;
}
get levelTargetDuration (): number {
get levelTargetDuration(): number {
return this.averagetargetduration || this.targetduration || DEFAULT_TARGET_DURATION;
}
get edge (): number {
get edge(): number {
return this.partEnd || this.fragmentEnd;
}
get partEnd (): number {
get partEnd(): number {
if (this.partList?.length) {
return this.partList[this.partList.length - 1].end;
}
return this.fragmentEnd;
}
get fragmentEnd (): number {
get fragmentEnd(): number {
if (this.fragments?.length) {
return this.fragments[this.fragments.length - 1].end;
}
return 0;
}
get age (): number {
get age(): number {
if (this.advancedDateTime) {
return Math.max(Date.now() - this.advancedDateTime, 0) / 1000;
}
return 0;
}
get lastPartIndex (): number {
get lastPartIndex(): number {
if (this.partList?.length) {
return this.partList[this.partList.length - 1].index;
}
return -1;
}
get lastPartSn (): number {
get lastPartSn(): number {
if (this.partList?.length) {
return this.partList[this.partList.length - 1].fragment.sn as number;
}
+4 -4
View File
@@ -9,15 +9,15 @@ export default class LevelKey {
public key: Uint8Array | null = null;
public iv: Uint8Array | null = null;
static fromURL (baseUrl: string, relativeUrl: string): LevelKey {
static fromURL(baseUrl: string, relativeUrl: string): LevelKey {
return new LevelKey(baseUrl, relativeUrl);
}
static fromURI (uri: string): LevelKey {
static fromURI(uri: string): LevelKey {
return new LevelKey(uri);
}
private constructor (absoluteOrBaseURI: string, relativeURL?: string) {
private constructor(absoluteOrBaseURI: string, relativeURL?: string) {
if (relativeURL) {
this._uri = buildAbsoluteURL(absoluteOrBaseURI, relativeURL, { alwaysNormalize: true });
} else {
@@ -25,7 +25,7 @@ export default class LevelKey {
}
}
get uri () {
get uri() {
return this._uri;
}
}
+215 -206
View File
@@ -18,42 +18,47 @@ type M3U8ParserFragments = Array<Fragment | null>;
const MASTER_PLAYLIST_REGEX = /#EXT-X-STREAM-INF:([^\n\r]*)[\r\n]+([^\r\n]+)|#EXT-X-SESSION-DATA:([^\n\r]*)[\r\n]+/g;
const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g;
const LEVEL_PLAYLIST_REGEX_FAST = new RegExp([
/#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source, // duration (#EXTINF:<duration>,<title>), group 1 => duration, group 2 => title
/(?!#) *(\S[\S ]*)/.source, // segment URI, group 3 => the URI (note newline is not eaten)
/#EXT-X-BYTERANGE:*(.+)/.source, // next segment's byterange, group 4 => range spec (x@y)
/#EXT-X-PROGRAM-DATE-TIME:(.+)/.source, // next segment's program date/time group 5 => the datetime spec
/#.*/.source // All other non-segment oriented tags will match with all groups empty
].join('|'), 'g');
const LEVEL_PLAYLIST_REGEX_FAST = new RegExp(
[
/#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source, // duration (#EXTINF:<duration>,<title>), group 1 => duration, group 2 => title
/(?!#) *(\S[\S ]*)/.source, // segment URI, group 3 => the URI (note newline is not eaten)
/#EXT-X-BYTERANGE:*(.+)/.source, // next segment's byterange, group 4 => range spec (x@y)
/#EXT-X-PROGRAM-DATE-TIME:(.+)/.source, // next segment's program date/time group 5 => the datetime spec
/#.*/.source, // All other non-segment oriented tags will match with all groups empty
].join('|'),
'g'
);
const LEVEL_PLAYLIST_REGEX_SLOW = new RegExp([
/#(EXTM3U)/.source,
/#EXT-X-(PLAYLIST-TYPE):(.+)/.source,
/#EXT-X-(MEDIA-SEQUENCE): *(\d+)/.source,
/#EXT-X-(SKIP):(.+)/.source,
/#EXT-X-(TARGETDURATION): *(\d+)/.source,
/#EXT-X-(KEY):(.+)/.source,
/#EXT-X-(START):(.+)/.source,
/#EXT-X-(ENDLIST)/.source,
/#EXT-X-(DISCONTINUITY-SEQ)UENCE: *(\d+)/.source,
/#EXT-X-(DIS)CONTINUITY/.source,
/#EXT-X-(VERSION):(\d+)/.source,
/#EXT-X-(MAP):(.+)/.source,
/#EXT-X-(SERVER-CONTROL):(.+)/.source,
/#EXT-X-(PART-INF):(.+)/.source,
/#EXT-X-(GAP)/.source,
/#EXT-X-(BITRATE):\s*(\d+)/.source,
/#EXT-X-(PART):(.+)/.source,
/#EXT-X-(PRELOAD-HINT):(.+)/.source,
/#EXT-X-(RENDITION-REPORT):(.+)/.source,
/(#)([^:]*):(.*)/.source,
/(#)(.*)(?:.*)\r?\n?/.source
].join('|'));
const LEVEL_PLAYLIST_REGEX_SLOW = new RegExp(
[
/#(EXTM3U)/.source,
/#EXT-X-(PLAYLIST-TYPE):(.+)/.source,
/#EXT-X-(MEDIA-SEQUENCE): *(\d+)/.source,
/#EXT-X-(SKIP):(.+)/.source,
/#EXT-X-(TARGETDURATION): *(\d+)/.source,
/#EXT-X-(KEY):(.+)/.source,
/#EXT-X-(START):(.+)/.source,
/#EXT-X-(ENDLIST)/.source,
/#EXT-X-(DISCONTINUITY-SEQ)UENCE: *(\d+)/.source,
/#EXT-X-(DIS)CONTINUITY/.source,
/#EXT-X-(VERSION):(\d+)/.source,
/#EXT-X-(MAP):(.+)/.source,
/#EXT-X-(SERVER-CONTROL):(.+)/.source,
/#EXT-X-(PART-INF):(.+)/.source,
/#EXT-X-(GAP)/.source,
/#EXT-X-(BITRATE):\s*(\d+)/.source,
/#EXT-X-(PART):(.+)/.source,
/#EXT-X-(PRELOAD-HINT):(.+)/.source,
/#EXT-X-(RENDITION-REPORT):(.+)/.source,
/(#)([^:]*):(.*)/.source,
/(#)(.*)(?:.*)\r?\n?/.source,
].join('|')
);
const MP4_REGEX_SUFFIX = /\.(mp4|m4s|m4v|m4a)$/i;
export default class M3U8Parser {
static findGroup (groups: Array<AudioGroup>, mediaGroupId: string): AudioGroup | undefined {
static findGroup(groups: Array<AudioGroup>, mediaGroupId: string): AudioGroup | undefined {
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
if (group.id === mediaGroupId) {
@@ -62,7 +67,7 @@ export default class M3U8Parser {
}
}
static convertAVC1ToAVCOTI (codec) {
static convertAVC1ToAVCOTI(codec) {
const avcdata = codec.split('.');
let result;
if (avcdata.length > 2) {
@@ -75,11 +80,11 @@ export default class M3U8Parser {
return result;
}
static resolve (url, baseUrl) {
static resolve(url, baseUrl) {
return URLToolkit.buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true });
}
static parseMasterPlaylist (string: string, baseurl: string) {
static parseMasterPlaylist(string: string, baseurl: string) {
const levels: Array<LevelParsed> = [];
const sessionData: Record<string, AttrList> = {};
let hasSessionData = false;
@@ -94,7 +99,7 @@ export default class M3U8Parser {
attrs,
bitrate: attrs.decimalInteger('AVERAGE-BANDWIDTH') || attrs.decimalInteger('BANDWIDTH'),
name: attrs.NAME,
url: M3U8Parser.resolve(result[2], baseurl)
url: M3U8Parser.resolve(result[2], baseurl),
};
const resolution = attrs.decimalResolution('RESOLUTION');
@@ -121,11 +126,11 @@ export default class M3U8Parser {
}
return {
levels,
sessionData: hasSessionData ? sessionData : null
sessionData: hasSessionData ? sessionData : null,
};
}
static parseMasterPlaylistMedia (string: string, baseurl: string, type: MediaPlaylistType, groups: Array<AudioGroup> = []): Array<MediaPlaylist> {
static parseMasterPlaylistMedia(string: string, baseurl: string, type: MediaPlaylistType, groups: Array<AudioGroup> = []): Array<MediaPlaylist> {
let result: RegExpExecArray | null;
const medias: Array<MediaPlaylist> = [];
let id = 0;
@@ -145,7 +150,7 @@ export default class M3U8Parser {
autoselect: attrs.bool('AUTOSELECT'),
forced: attrs.bool('FORCED'),
lang: attrs.LANGUAGE,
url: attrs.URI ? M3U8Parser.resolve(attrs.URI, baseurl) : ''
url: attrs.URI ? M3U8Parser.resolve(attrs.URI, baseurl) : '',
};
if (groups.length) {
@@ -163,7 +168,7 @@ export default class M3U8Parser {
return medias;
}
static parseLevelPlaylist (string: string, baseurl: string, id: number, type: PlaylistLevelType, levelUrlId: number): LevelDetails {
static parseLevelPlaylist(string: string, baseurl: string, id: number, type: PlaylistLevelType, levelUrlId: number): LevelDetails {
const level = new LevelDetails(baseurl);
const fragments: M3U8ParserFragments = level.fragments;
let currentSN = 0;
@@ -182,13 +187,15 @@ export default class M3U8Parser {
while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) {
const duration = result[1];
if (duration) { // INF
if (duration) {
// INF
frag.duration = parseFloat(duration);
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
const title = (' ' + result[2]).slice(1);
frag.title = title || null;
frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]);
} else if (result[3]) { // url
} else if (result[3]) {
// url
if (Number.isFinite(frag.duration)) {
frag.start = totalduration;
if (levelkey) {
@@ -214,14 +221,16 @@ export default class M3U8Parser {
frag.cc = discontinuityCounter;
frag.level = id;
}
} else if (result[4]) { // X-BYTERANGE
} else if (result[4]) {
// X-BYTERANGE
const data = (' ' + result[4]).slice(1);
if (prevFrag) {
frag.setByteRange(data, prevFrag);
} else {
frag.setByteRange(data);
}
} else if (result[5]) { // PROGRAM-DATE-TIME
} else if (result[5]) {
// PROGRAM-DATE-TIME
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
frag.rawProgramDateTime = (' ' + result[5]).slice(1);
frag.tagList.push(['PROGRAM-DATE-TIME', frag.rawProgramDateTime]);
@@ -246,174 +255,174 @@ export default class M3U8Parser {
const value2 = result[i + 2] ? (' ' + result[i + 2]).slice(1) : '';
switch (tag) {
case 'PLAYLIST-TYPE':
level.type = value1.toUpperCase();
break;
case 'MEDIA-SEQUENCE':
currentSN = level.startSN = parseInt(value1);
break;
case 'SKIP': {
const skipAttrs = new AttrList(value1);
const skippedSegments = skipAttrs.decimalInteger('SKIPPED-SEGMENTS');
if (Number.isFinite(skippedSegments)) {
level.skippedSegments = skippedSegments;
// This will result in fragments[] containing undefined values, which we will fill in with `mergeDetails`
for (let i = skippedSegments; i--;) {
fragments.unshift(null);
case 'PLAYLIST-TYPE':
level.type = value1.toUpperCase();
break;
case 'MEDIA-SEQUENCE':
currentSN = level.startSN = parseInt(value1);
break;
case 'SKIP': {
const skipAttrs = new AttrList(value1);
const skippedSegments = skipAttrs.decimalInteger('SKIPPED-SEGMENTS');
if (Number.isFinite(skippedSegments)) {
level.skippedSegments = skippedSegments;
// This will result in fragments[] containing undefined values, which we will fill in with `mergeDetails`
for (let i = skippedSegments; i--; ) {
fragments.unshift(null);
}
currentSN += skippedSegments;
}
currentSN += skippedSegments;
const recentlyRemovedDateranges = skipAttrs.enumeratedString('RECENTLY-REMOVED-DATERANGES');
if (recentlyRemovedDateranges) {
level.recentlyRemovedDateranges = recentlyRemovedDateranges.split('\t');
}
break;
}
const recentlyRemovedDateranges = skipAttrs.enumeratedString('RECENTLY-REMOVED-DATERANGES');
if (recentlyRemovedDateranges) {
level.recentlyRemovedDateranges = recentlyRemovedDateranges.split('\t');
}
break;
}
case 'TARGETDURATION':
level.targetduration = parseFloat(value1);
break;
case 'VERSION':
level.version = parseInt(value1);
break;
case 'EXTM3U':
break;
case 'ENDLIST':
level.live = false;
break;
case '#':
if (value1 || value2) {
frag.tagList.push(value2 ? [value1, value2] : [value1]);
}
break;
case 'DIS':
discontinuityCounter++;
case 'TARGETDURATION':
level.targetduration = parseFloat(value1);
break;
case 'VERSION':
level.version = parseInt(value1);
break;
case 'EXTM3U':
break;
case 'ENDLIST':
level.live = false;
break;
case '#':
if (value1 || value2) {
frag.tagList.push(value2 ? [value1, value2] : [value1]);
}
break;
case 'DIS':
discontinuityCounter++;
/* falls through */
case 'GAP':
frag.tagList.push([tag]);
break;
case 'BITRATE':
frag.tagList.push([tag, value1]);
break;
case 'DISCONTINUITY-SEQ':
discontinuityCounter = parseInt(value1);
break;
case 'KEY': {
// https://tools.ietf.org/html/rfc8216#section-4.3.2.4
const keyAttrs = new AttrList(value1);
const decryptmethod = keyAttrs.enumeratedString('METHOD');
const decrypturi = keyAttrs.URI;
const decryptiv = keyAttrs.hexadecimalInteger('IV');
const decryptkeyformatversions = keyAttrs.enumeratedString('KEYFORMATVERSIONS');
const decryptkeyid = keyAttrs.enumeratedString('KEYID');
// From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity".
const decryptkeyformat = keyAttrs.enumeratedString('KEYFORMAT') ?? 'identity';
case 'GAP':
frag.tagList.push([tag]);
break;
case 'BITRATE':
frag.tagList.push([tag, value1]);
break;
case 'DISCONTINUITY-SEQ':
discontinuityCounter = parseInt(value1);
break;
case 'KEY': {
// https://tools.ietf.org/html/rfc8216#section-4.3.2.4
const keyAttrs = new AttrList(value1);
const decryptmethod = keyAttrs.enumeratedString('METHOD');
const decrypturi = keyAttrs.URI;
const decryptiv = keyAttrs.hexadecimalInteger('IV');
const decryptkeyformatversions = keyAttrs.enumeratedString('KEYFORMATVERSIONS');
const decryptkeyid = keyAttrs.enumeratedString('KEYID');
// From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity".
const decryptkeyformat = keyAttrs.enumeratedString('KEYFORMAT') ?? 'identity';
const unsupportedKnownKeyformatsInManifest = [
'com.apple.streamingkeydelivery',
'com.microsoft.playready',
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', // widevine (v2)
'com.widevine' // earlier widevine (v1)
];
const unsupportedKnownKeyformatsInManifest = [
'com.apple.streamingkeydelivery',
'com.microsoft.playready',
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', // widevine (v2)
'com.widevine', // earlier widevine (v1)
];
if (unsupportedKnownKeyformatsInManifest.indexOf(decryptkeyformat) > -1) {
logger.warn(`Keyformat ${decryptkeyformat} is not supported from the manifest`);
continue;
} else if (decryptkeyformat !== 'identity') {
// We are supposed to skip keys we don't understand.
// As we currently only officially support identity keys
// from the manifest we shouldn't save any other key.
continue;
}
// TODO: multiple keys can be defined on a fragment, and we need to support this
// for clients that support both playready and widevine
if (decryptmethod) {
// TODO: need to determine if the level key is actually a relative URL
// if it isn't, then we should instead construct the LevelKey using fromURI.
levelkey = LevelKey.fromURL(baseurl, decrypturi);
if ((decrypturi) && (['AES-128', 'SAMPLE-AES', 'SAMPLE-AES-CENC'].indexOf(decryptmethod) >= 0)) {
levelkey.method = decryptmethod;
levelkey.keyFormat = decryptkeyformat;
if (decryptkeyid) {
levelkey.keyID = decryptkeyid;
}
if (decryptkeyformatversions) {
levelkey.keyFormatVersions = decryptkeyformatversions;
}
// Initialization Vector (IV)
levelkey.iv = decryptiv;
if (unsupportedKnownKeyformatsInManifest.indexOf(decryptkeyformat) > -1) {
logger.warn(`Keyformat ${decryptkeyformat} is not supported from the manifest`);
continue;
} else if (decryptkeyformat !== 'identity') {
// We are supposed to skip keys we don't understand.
// As we currently only officially support identity keys
// from the manifest we shouldn't save any other key.
continue;
}
// TODO: multiple keys can be defined on a fragment, and we need to support this
// for clients that support both playready and widevine
if (decryptmethod) {
// TODO: need to determine if the level key is actually a relative URL
// if it isn't, then we should instead construct the LevelKey using fromURI.
levelkey = LevelKey.fromURL(baseurl, decrypturi);
if (decrypturi && ['AES-128', 'SAMPLE-AES', 'SAMPLE-AES-CENC'].indexOf(decryptmethod) >= 0) {
levelkey.method = decryptmethod;
levelkey.keyFormat = decryptkeyformat;
if (decryptkeyid) {
levelkey.keyID = decryptkeyid;
}
if (decryptkeyformatversions) {
levelkey.keyFormatVersions = decryptkeyformatversions;
}
// Initialization Vector (IV)
levelkey.iv = decryptiv;
}
}
break;
}
break;
}
case 'START': {
const startAttrs = new AttrList(value1);
const startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET');
// TIME-OFFSET can be 0
if (Number.isFinite(startTimeOffset)) {
level.startTimeOffset = startTimeOffset;
case 'START': {
const startAttrs = new AttrList(value1);
const startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET');
// TIME-OFFSET can be 0
if (Number.isFinite(startTimeOffset)) {
level.startTimeOffset = startTimeOffset;
}
break;
}
break;
}
case 'MAP': {
const mapAttrs = new AttrList(value1);
frag.relurl = mapAttrs.URI;
if (mapAttrs.BYTERANGE) {
frag.setByteRange(mapAttrs.BYTERANGE);
case 'MAP': {
const mapAttrs = new AttrList(value1);
frag.relurl = mapAttrs.URI;
if (mapAttrs.BYTERANGE) {
frag.setByteRange(mapAttrs.BYTERANGE);
}
frag.level = id;
frag.sn = 'initSegment';
if (levelkey) {
frag.levelkey = levelkey;
}
level.initSegment = frag;
frag = new Fragment(type, baseurl);
frag.rawProgramDateTime = level.initSegment.rawProgramDateTime;
break;
}
frag.level = id;
frag.sn = 'initSegment';
if (levelkey) {
frag.levelkey = levelkey;
case 'SERVER-CONTROL': {
const serverControlAttrs = new AttrList(value1);
level.canBlockReload = serverControlAttrs.bool('CAN-BLOCK-RELOAD');
level.canSkipUntil = serverControlAttrs.optionalFloat('CAN-SKIP-UNTIL', 0);
level.canSkipDateRanges = level.canSkipUntil > 0 && serverControlAttrs.bool('CAN-SKIP-DATERANGES');
level.partHoldBack = serverControlAttrs.optionalFloat('PART-HOLD-BACK', 0);
level.holdBack = serverControlAttrs.optionalFloat('HOLD-BACK', 0);
break;
}
level.initSegment = frag;
frag = new Fragment(type, baseurl);
frag.rawProgramDateTime = level.initSegment.rawProgramDateTime;
break;
}
case 'SERVER-CONTROL': {
const serverControlAttrs = new AttrList(value1);
level.canBlockReload = serverControlAttrs.bool('CAN-BLOCK-RELOAD');
level.canSkipUntil = serverControlAttrs.optionalFloat('CAN-SKIP-UNTIL', 0);
level.canSkipDateRanges = level.canSkipUntil > 0 && serverControlAttrs.bool('CAN-SKIP-DATERANGES');
level.partHoldBack = serverControlAttrs.optionalFloat('PART-HOLD-BACK', 0);
level.holdBack = serverControlAttrs.optionalFloat('HOLD-BACK', 0);
break;
}
case 'PART-INF': {
const partInfAttrs = new AttrList(value1);
level.partTarget = partInfAttrs.decimalFloatingPoint('PART-TARGET');
break;
}
case 'PART': {
let partList = level.partList;
if (!partList) {
partList = level.partList = [];
case 'PART-INF': {
const partInfAttrs = new AttrList(value1);
level.partTarget = partInfAttrs.decimalFloatingPoint('PART-TARGET');
break;
}
const previousFragmentPart = currentPart > 0 ? partList[partList.length - 1] : undefined;
const index = currentPart++;
const part = new Part(new AttrList(value1), frag, baseurl, index, previousFragmentPart);
partList.push(part);
frag.duration += part.duration;
break;
}
case 'PRELOAD-HINT': {
const preloadHintAttrs = new AttrList(value1);
level.preloadHint = preloadHintAttrs;
break;
}
case 'RENDITION-REPORT': {
const renditionReportAttrs = new AttrList(value1);
level.renditionReports = level.renditionReports || [];
level.renditionReports.push(renditionReportAttrs);
break;
}
default:
logger.warn(`line parsed but not handled: ${result}`);
break;
case 'PART': {
let partList = level.partList;
if (!partList) {
partList = level.partList = [];
}
const previousFragmentPart = currentPart > 0 ? partList[partList.length - 1] : undefined;
const index = currentPart++;
const part = new Part(new AttrList(value1), frag, baseurl, index, previousFragmentPart);
partList.push(part);
frag.duration += part.duration;
break;
}
case 'PRELOAD-HINT': {
const preloadHintAttrs = new AttrList(value1);
level.preloadHint = preloadHintAttrs;
break;
}
case 'RENDITION-REPORT': {
const renditionReportAttrs = new AttrList(value1);
level.renditionReports = level.renditionReports || [];
level.renditionReports.push(renditionReportAttrs);
break;
}
default:
logger.warn(`line parsed but not handled: ${result}`);
break;
}
}
}
@@ -476,7 +485,7 @@ export default class M3U8Parser {
}
}
function setCodecs (codecs: Array<string>, level: LevelParsed) {
function setCodecs(codecs: Array<string>, level: LevelParsed) {
['video', 'audio', 'text'].forEach((type: CodecType) => {
const filtered = codecs.filter((codec) => isCodecType(codec, type));
if (filtered.length) {
@@ -493,27 +502,27 @@ function setCodecs (codecs: Array<string>, level: LevelParsed) {
level.unknownCodecs = codecs;
}
function assignCodec (media, groupItem, codecProperty) {
function assignCodec(media, groupItem, codecProperty) {
const codecValue = groupItem[codecProperty];
if (codecValue) {
media[codecProperty] = codecValue;
}
}
function backfillProgramDateTimes (fragments: M3U8ParserFragments, firstPdtIndex: number) {
function backfillProgramDateTimes(fragments: M3U8ParserFragments, firstPdtIndex: number) {
let fragPrev = fragments[firstPdtIndex] as Fragment;
for (let i = firstPdtIndex; i--;) {
for (let i = firstPdtIndex; i--; ) {
const frag = fragments[i];
// Exit on delta-playlist skipped segments
if (!frag) {
return;
}
frag.programDateTime = (fragPrev.programDateTime as number) - (frag.duration * 1000);
frag.programDateTime = (fragPrev.programDateTime as number) - frag.duration * 1000;
fragPrev = frag;
}
}
function assignProgramDateTime (frag, prevFrag) {
function assignProgramDateTime(frag, prevFrag) {
if (frag.rawProgramDateTime) {
frag.programDateTime = Date.parse(frag.rawProgramDateTime);
} else if (prevFrag?.programDateTime) {
+122 -119
View File
@@ -23,20 +23,20 @@ import type Hls from '../hls';
import AttrList from '../utils/attr-list';
import type { ErrorData, LevelLoadingData, ManifestLoadingData, TrackLoadingData } from '../types/events';
function mapContextToLevelType (context: PlaylistLoaderContext): PlaylistLevelType {
function mapContextToLevelType(context: PlaylistLoaderContext): PlaylistLevelType {
const { type } = context;
switch (type) {
case PlaylistContextType.AUDIO_TRACK:
return PlaylistLevelType.AUDIO;
case PlaylistContextType.SUBTITLE_TRACK:
return PlaylistLevelType.SUBTITLE;
default:
return PlaylistLevelType.MAIN;
case PlaylistContextType.AUDIO_TRACK:
return PlaylistLevelType.AUDIO;
case PlaylistContextType.SUBTITLE_TRACK:
return PlaylistLevelType.SUBTITLE;
default:
return PlaylistLevelType.MAIN;
}
}
function getResponseUrl (response: LoaderResponse, context: PlaylistLoaderContext): string {
function getResponseUrl(response: LoaderResponse, context: PlaylistLoaderContext): string {
let url = response.url;
// responseURL not supported on some browsers (it is used to detect URL redirection)
// data-uri mode also not supported (but no need to detect redirection)
@@ -50,17 +50,17 @@ function getResponseUrl (response: LoaderResponse, context: PlaylistLoaderContex
class PlaylistLoader {
private readonly hls: Hls;
private readonly loaders: {
[key: string]: Loader<LoaderContext>
} = Object.create(null)
[key: string]: Loader<LoaderContext>;
} = Object.create(null);
private checkAgeHeader: boolean = true;
constructor (hls: Hls) {
constructor(hls: Hls) {
this.hls = hls;
this.registerListeners();
}
private registerListeners () {
private registerListeners() {
const { hls } = this;
hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this);
@@ -68,7 +68,7 @@ class PlaylistLoader {
hls.on(Events.SUBTITLE_TRACK_LOADING, this.onSubtitleTrackLoading, this);
}
private unregisterListeners () {
private unregisterListeners() {
const { hls } = this;
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this);
@@ -79,7 +79,7 @@ class PlaylistLoader {
/**
* Returns defaults or configured loader-type overloads (pLoader and loader config params)
*/
private createInternalLoader (context: PlaylistLoaderContext): Loader<LoaderContext> {
private createInternalLoader(context: PlaylistLoaderContext): Loader<LoaderContext> {
const config = this.hls.config;
const PLoader = config.pLoader;
const Loader = config.loader;
@@ -93,11 +93,11 @@ class PlaylistLoader {
return loader;
}
private getInternalLoader (context: PlaylistLoaderContext): Loader<LoaderContext> {
private getInternalLoader(context: PlaylistLoaderContext): Loader<LoaderContext> {
return this.loaders[context.type];
}
private resetInternalLoader (contextType): void {
private resetInternalLoader(contextType): void {
if (this.loaders[contextType]) {
delete this.loaders[contextType];
}
@@ -106,7 +106,7 @@ class PlaylistLoader {
/**
* Call `destroy` on all internal loader instances mapped (one per context type)
*/
private destroyInternalLoaders (): void {
private destroyInternalLoaders(): void {
for (const contextType in this.loaders) {
const loader = this.loaders[contextType];
if (loader) {
@@ -117,12 +117,12 @@ class PlaylistLoader {
}
}
public destroy (): void {
public destroy(): void {
this.unregisterListeners();
this.destroyInternalLoaders();
}
private onManifestLoading (event: Events.MANIFEST_LOADING, data: ManifestLoadingData) {
private onManifestLoading(event: Events.MANIFEST_LOADING, data: ManifestLoadingData) {
const { url } = data;
this.checkAgeHeader = true;
this.load({
@@ -132,11 +132,11 @@ class PlaylistLoader {
responseType: 'text',
type: PlaylistContextType.MANIFEST,
url,
deliveryDirectives: null
deliveryDirectives: null,
});
}
private onLevelLoading (event: Events.LEVEL_LOADING, data: LevelLoadingData) {
private onLevelLoading(event: Events.LEVEL_LOADING, data: LevelLoadingData) {
const { id, level, url, deliveryDirectives } = data;
this.load({
id,
@@ -145,11 +145,11 @@ class PlaylistLoader {
responseType: 'text',
type: PlaylistContextType.LEVEL,
url,
deliveryDirectives
deliveryDirectives,
});
}
private onAudioTrackLoading (event: Events.AUDIO_TRACK_LOADING, data: TrackLoadingData) {
private onAudioTrackLoading(event: Events.AUDIO_TRACK_LOADING, data: TrackLoadingData) {
const { id, groupId, url, deliveryDirectives } = data;
this.load({
id,
@@ -158,11 +158,11 @@ class PlaylistLoader {
responseType: 'text',
type: PlaylistContextType.AUDIO_TRACK,
url,
deliveryDirectives
deliveryDirectives,
});
}
private onSubtitleTrackLoading (event: Events.SUBTITLE_TRACK_LOADING, data: TrackLoadingData) {
private onSubtitleTrackLoading(event: Events.SUBTITLE_TRACK_LOADING, data: TrackLoadingData) {
const { id, groupId, url, deliveryDirectives } = data;
this.load({
id,
@@ -171,11 +171,11 @@ class PlaylistLoader {
responseType: 'text',
type: PlaylistContextType.SUBTITLE_TRACK,
url,
deliveryDirectives
deliveryDirectives,
});
}
private load (context: PlaylistLoaderContext): void {
private load(context: PlaylistLoaderContext): void {
const config = this.hls.config;
// logger.debug(`[playlist-loader]: Loading playlist of type ${context.type}, level: ${context.level}, id: ${context.id}`);
@@ -184,7 +184,8 @@ class PlaylistLoader {
let loader = this.getInternalLoader(context);
if (loader) {
const loaderContext = loader.context;
if (loaderContext && loaderContext.url === context.url) { // same URL can't overlap
if (loaderContext && loaderContext.url === context.url) {
// same URL can't overlap
logger.trace('[playlist-loader]: playlist request ongoing');
return;
}
@@ -200,25 +201,25 @@ class PlaylistLoader {
// apply different configs for retries depending on
// context (manifest, level, audio/subs playlist)
switch (context.type) {
case PlaylistContextType.MANIFEST:
maxRetry = config.manifestLoadingMaxRetry;
timeout = config.manifestLoadingTimeOut;
retryDelay = config.manifestLoadingRetryDelay;
maxRetryDelay = config.manifestLoadingMaxRetryTimeout;
break;
case PlaylistContextType.LEVEL:
case PlaylistContextType.AUDIO_TRACK:
case PlaylistContextType.SUBTITLE_TRACK:
// Manage retries in Level/Track Controller
maxRetry = 0;
timeout = config.levelLoadingTimeOut;
break;
default:
maxRetry = config.levelLoadingMaxRetry;
timeout = config.levelLoadingTimeOut;
retryDelay = config.levelLoadingRetryDelay;
maxRetryDelay = config.levelLoadingMaxRetryTimeout;
break;
case PlaylistContextType.MANIFEST:
maxRetry = config.manifestLoadingMaxRetry;
timeout = config.manifestLoadingTimeOut;
retryDelay = config.manifestLoadingRetryDelay;
maxRetryDelay = config.manifestLoadingMaxRetryTimeout;
break;
case PlaylistContextType.LEVEL:
case PlaylistContextType.AUDIO_TRACK:
case PlaylistContextType.SUBTITLE_TRACK:
// Manage retries in Level/Track Controller
maxRetry = 0;
timeout = config.levelLoadingTimeOut;
break;
default:
maxRetry = config.levelLoadingMaxRetry;
timeout = config.levelLoadingTimeOut;
retryDelay = config.levelLoadingRetryDelay;
maxRetryDelay = config.levelLoadingMaxRetryTimeout;
break;
}
loader = this.createInternalLoader(context);
@@ -248,13 +249,13 @@ class PlaylistLoader {
maxRetry,
retryDelay,
maxRetryDelay,
highWaterMark: 0
highWaterMark: 0,
};
const loaderCallbacks = {
onSuccess: this.loadsuccess.bind(this),
onError: this.loaderror.bind(this),
onTimeout: this.loadtimeout.bind(this)
onTimeout: this.loadtimeout.bind(this),
};
// logger.debug(`[playlist-loader]: Calling internal loader delegate for URL: ${context.url}`);
@@ -262,7 +263,7 @@ class PlaylistLoader {
loader.load(context, loaderConfig, loaderCallbacks);
}
private loadsuccess (response: LoaderResponse, stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: any = null): void {
private loadsuccess(response: LoaderResponse, stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: any = null): void {
if (context.isSidxRequest) {
this.handleSidxRequest(response, context);
this.handlePlaylistLoaded(response, stats, context, networkDetails);
@@ -288,15 +289,15 @@ class PlaylistLoader {
}
}
private loaderror (response: LoaderResponse, context: PlaylistLoaderContext, networkDetails: any = null): void {
private loaderror(response: LoaderResponse, context: PlaylistLoaderContext, networkDetails: any = null): void {
this.handleNetworkError(context, networkDetails, false, response);
}
private loadtimeout (stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: any = null): void {
private loadtimeout(stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: any = null): void {
this.handleNetworkError(context, networkDetails, true);
}
private handleMasterPlaylist (response: LoaderResponse, stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: any): void {
private handleMasterPlaylist(response: LoaderResponse, stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: any): void {
const hls = this.hls;
const string = response.data as string;
@@ -311,12 +312,12 @@ class PlaylistLoader {
// multi level playlist, parse level info
const audioGroups = levels.map((level: LevelParsed) => ({
id: level.attrs.AUDIO,
audioCodec: level.audioCodec
audioCodec: level.audioCodec,
}));
const subtitleGroups = levels.map((level: LevelParsed) => ({
id: level.attrs.SUBTITLES,
textCodec: level.textCodec
textCodec: level.textCodec,
}));
const audioTracks = M3U8Parser.parseMasterPlaylistMedia(string, url, 'AUDIO', audioGroups);
@@ -325,7 +326,7 @@ class PlaylistLoader {
if (audioTracks.length) {
// check if we have found an audio track embedded in main playlist (audio track without URI attribute)
const embeddedAudioFound: boolean = audioTracks.some(audioTrack => !audioTrack.url);
const embeddedAudioFound: boolean = audioTracks.some((audioTrack) => !audioTrack.url);
// if no embedded audio track defined, but audio codec signaled in quality level,
// we need to signal this main audio track this could happen with playlists with
@@ -342,7 +343,7 @@ class PlaylistLoader {
id: -1,
attrs: new AttrList({}),
bitrate: 0,
url: ''
url: '',
});
}
}
@@ -355,11 +356,11 @@ class PlaylistLoader {
url,
stats,
networkDetails,
sessionData
sessionData,
});
}
private handleTrackOrLevelPlaylist (response: LoaderResponse, stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: any): void {
private handleTrackOrLevelPlaylist(response: LoaderResponse, stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: any): void {
const hls = this.hls;
const { id, level, type } = context;
@@ -376,7 +377,7 @@ class PlaylistLoader {
fatal: false,
url: url,
reason: 'no fragments found in level',
level: typeof context.level === 'number' ? context.level : undefined
level: typeof context.level === 'number' ? context.level : undefined,
});
return;
}
@@ -391,7 +392,7 @@ class PlaylistLoader {
bitrate: 0,
details: levelDetails,
name: '',
url
url,
};
hls.trigger(Events.MANIFEST_LOADED, {
@@ -400,7 +401,7 @@ class PlaylistLoader {
url,
stats,
networkDetails,
sessionData: null
sessionData: null,
});
}
@@ -423,7 +424,7 @@ class PlaylistLoader {
rangeStart: 0,
rangeEnd: 2048,
responseType: 'arraybuffer',
deliveryDirectives: null
deliveryDirectives: null,
});
return;
}
@@ -434,7 +435,7 @@ class PlaylistLoader {
this.handlePlaylistLoaded(response, stats, context, networkDetails);
}
private handleSidxRequest (response: LoaderResponse, context: PlaylistLoaderContext): void {
private handleSidxRequest(response: LoaderResponse, context: PlaylistLoaderContext): void {
const sidxInfo = parseSegmentIndex(new Uint8Array(response.data as ArrayBuffer));
// if provided fragment does not contain sidx, early return
if (!sidxInfo) {
@@ -453,7 +454,7 @@ class PlaylistLoader {
(levelDetails.initSegment as Fragment).setByteRange(String(sidxInfo.moovEndOffset) + '@0');
}
private handleManifestParsingError (response: LoaderResponse, context: PlaylistLoaderContext, reason: string, networkDetails: any): void {
private handleManifestParsingError(response: LoaderResponse, context: PlaylistLoaderContext, reason: string, networkDetails: any): void {
this.hls.trigger(Events.ERROR, {
type: ErrorTypes.NETWORK_ERROR,
details: ErrorDetails.MANIFEST_PARSING_ERROR,
@@ -462,36 +463,38 @@ class PlaylistLoader {
reason,
response,
context,
networkDetails
networkDetails,
});
}
private handleNetworkError (context: PlaylistLoaderContext, networkDetails: any, timeout = false, response?: LoaderResponse): void {
logger.warn(`[playlist-loader]: A network ${
timeout ? 'timeout' : 'error'
} occurred while loading ${context.type} level: ${context.level} id: ${context.id} group-id: "${context.groupId}"`);
private handleNetworkError(context: PlaylistLoaderContext, networkDetails: any, timeout = false, response?: LoaderResponse): void {
logger.warn(
`[playlist-loader]: A network ${timeout ? 'timeout' : 'error'} occurred while loading ${context.type} level: ${context.level} id: ${context.id} group-id: "${
context.groupId
}"`
);
let details = ErrorDetails.UNKNOWN;
let fatal = false;
const loader = this.getInternalLoader(context);
switch (context.type) {
case PlaylistContextType.MANIFEST:
details = (timeout ? ErrorDetails.MANIFEST_LOAD_TIMEOUT : ErrorDetails.MANIFEST_LOAD_ERROR);
fatal = true;
break;
case PlaylistContextType.LEVEL:
details = (timeout ? ErrorDetails.LEVEL_LOAD_TIMEOUT : ErrorDetails.LEVEL_LOAD_ERROR);
fatal = false;
break;
case PlaylistContextType.AUDIO_TRACK:
details = (timeout ? ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT : ErrorDetails.AUDIO_TRACK_LOAD_ERROR);
fatal = false;
break;
case PlaylistContextType.SUBTITLE_TRACK:
details = (timeout ? ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT : ErrorDetails.SUBTITLE_LOAD_ERROR);
fatal = false;
break;
case PlaylistContextType.MANIFEST:
details = timeout ? ErrorDetails.MANIFEST_LOAD_TIMEOUT : ErrorDetails.MANIFEST_LOAD_ERROR;
fatal = true;
break;
case PlaylistContextType.LEVEL:
details = timeout ? ErrorDetails.LEVEL_LOAD_TIMEOUT : ErrorDetails.LEVEL_LOAD_ERROR;
fatal = false;
break;
case PlaylistContextType.AUDIO_TRACK:
details = timeout ? ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT : ErrorDetails.AUDIO_TRACK_LOAD_ERROR;
fatal = false;
break;
case PlaylistContextType.SUBTITLE_TRACK:
details = timeout ? ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT : ErrorDetails.SUBTITLE_LOAD_ERROR;
fatal = false;
break;
}
if (loader) {
@@ -505,7 +508,7 @@ class PlaylistLoader {
url: context.url,
loader,
context,
networkDetails
networkDetails,
};
if (response) {
@@ -515,7 +518,7 @@ class PlaylistLoader {
this.hls.trigger(Events.ERROR, errorData);
}
private handlePlaylistLoaded (response: LoaderResponse, stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: any): void {
private handlePlaylistLoaded(response: LoaderResponse, stats: LoaderStats, context: PlaylistLoaderContext, networkDetails: any): void {
const { type, level, id, groupId, loader, levelDetails, deliveryDirectives } = context;
if (!levelDetails?.targetduration) {
@@ -533,37 +536,37 @@ class PlaylistLoader {
this.checkAgeHeader = !!ageHeader;
switch (type) {
case PlaylistContextType.MANIFEST:
case PlaylistContextType.LEVEL:
this.hls.trigger(Events.LEVEL_LOADED, {
details: levelDetails,
level: level || 0,
id: id || 0,
stats,
networkDetails,
deliveryDirectives
});
break;
case PlaylistContextType.AUDIO_TRACK:
this.hls.trigger(Events.AUDIO_TRACK_LOADED, {
details: levelDetails,
id: id || 0,
groupId: groupId || '',
stats,
networkDetails,
deliveryDirectives
});
break;
case PlaylistContextType.SUBTITLE_TRACK:
this.hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
details: levelDetails,
id: id || 0,
groupId: groupId || '',
stats,
networkDetails,
deliveryDirectives
});
break;
case PlaylistContextType.MANIFEST:
case PlaylistContextType.LEVEL:
this.hls.trigger(Events.LEVEL_LOADED, {
details: levelDetails,
level: level || 0,
id: id || 0,
stats,
networkDetails,
deliveryDirectives,
});
break;
case PlaylistContextType.AUDIO_TRACK:
this.hls.trigger(Events.AUDIO_TRACK_LOADED, {
details: levelDetails,
id: id || 0,
groupId: groupId || '',
stats,
networkDetails,
deliveryDirectives,
});
break;
case PlaylistContextType.SUBTITLE_TRACK:
this.hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
details: levelDetails,
id: id || 0,
groupId: groupId || '',
stats,
networkDetails,
deliveryDirectives,
});
break;
}
}
}
+9 -9
View File
@@ -15,21 +15,21 @@ import type { FragBufferedData } from '../types/events';
export default class PerformanceMonitor {
private hls: Hls;
constructor (hls: Hls) {
constructor(hls: Hls) {
this.hls = hls;
this.hls.on(Events.FRAG_BUFFERED, this.onFragBuffered);
}
destroy () {
destroy() {
this.hls.off(Events.FRAG_BUFFERED);
}
onFragBuffered (event: Events.FRAG_BUFFERED, data: FragBufferedData) {
onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
logFragStats(data);
}
}
function logFragStats (data: FragBufferedData) {
function logFragStats(data: FragBufferedData) {
const { frag, part } = data;
const stats = part ? part.stats : frag.stats;
const tLoad = stats.loading.end - stats.loading.start;
@@ -37,8 +37,8 @@ function logFragStats (data: FragBufferedData) {
const tParse = stats.parsing.end - stats.parsing.start;
const tTotal = stats.buffering.end - stats.loading.start;
logger.log(`[performance-monitor]: Stats for fragment ${frag.sn} ${part ? (' part ' + part.index) : ''} of level ${frag.level}:
Size: ${((stats.total / 1024)).toFixed(3)} kB
logger.log(`[performance-monitor]: Stats for fragment ${frag.sn} ${part ? ' part ' + part.index : ''} of level ${frag.level}:
Size: ${(stats.total / 1024).toFixed(3)} kB
Chunk Count: ${stats.chunkCount}
Request: ${stats.loading.start.toFixed(3)} ms
@@ -50,7 +50,7 @@ function logFragStats (data: FragBufferedData) {
Buffering End: ${stats.buffering.end.toFixed(3)} ms
Load Duration: ${tLoad.toFixed(3)} ms
Parse Duration: ${(tParse).toFixed(3)} ms
Buffer Duration: ${(tBuffer).toFixed(3)} ms
End-To-End Duration: ${(tTotal).toFixed(3)} ms`);
Parse Duration: ${tParse.toFixed(3)} ms
Buffer Duration: ${tBuffer.toFixed(3)} ms
End-To-End Duration: ${tTotal.toFixed(3)} ms`);
}
+5 -3
View File
@@ -1,5 +1,7 @@
export const isFiniteNumber = Number.isFinite || function (value) {
return typeof value === 'number' && isFinite(value);
};
export const isFiniteNumber =
Number.isFinite ||
function (value) {
return typeof value === 'number' && isFinite(value);
};
export const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991;
+236 -29
View File
@@ -3,37 +3,244 @@
*/
class AAC {
static getSilentFrame (codec?: string, channelCount?: number): Uint8Array | undefined {
static getSilentFrame(codec?: string, channelCount?: number): Uint8Array | undefined {
switch (codec) {
case 'mp4a.40.2':
if (channelCount === 1) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x23, 0x80]);
} else if (channelCount === 2) {
return new Uint8Array([0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23, 0x80]);
} else if (channelCount === 3) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x8e]);
} else if (channelCount === 4) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x80, 0x2c, 0x80, 0x08, 0x02, 0x38]);
} else if (channelCount === 5) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x38]);
} else if (channelCount === 6) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x00, 0xb2, 0x00, 0x20, 0x08, 0xe0]);
}
case 'mp4a.40.2':
if (channelCount === 1) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x23, 0x80]);
} else if (channelCount === 2) {
return new Uint8Array([0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23, 0x80]);
} else if (channelCount === 3) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x8e]);
} else if (channelCount === 4) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x80, 0x2c, 0x80, 0x08, 0x02, 0x38]);
} else if (channelCount === 5) {
return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x38]);
} else if (channelCount === 6) {
return new Uint8Array([
0x00,
0xc8,
0x00,
0x80,
0x20,
0x84,
0x01,
0x26,
0x40,
0x08,
0x64,
0x00,
0x82,
0x30,
0x04,
0x99,
0x00,
0x21,
0x90,
0x02,
0x00,
0xb2,
0x00,
0x20,
0x08,
0xe0,
]);
}
break;
// handle HE-AAC below (mp4a.40.5 / mp4a.40.29)
default:
if (channelCount === 1) {
// ffmpeg -y -f lavfi -i "aevalsrc=0:d=0.05" -c:a libfdk_aac -profile:a aac_he -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac
return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x4e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x1c, 0x6, 0xf1, 0xc1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]);
} else if (channelCount === 2) {
// ffmpeg -y -f lavfi -i "aevalsrc=0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac
return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]);
} else if (channelCount === 3) {
// ffmpeg -y -f lavfi -i "aevalsrc=0|0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac
return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]);
}
break;
break;
// handle HE-AAC below (mp4a.40.5 / mp4a.40.29)
default:
if (channelCount === 1) {
// ffmpeg -y -f lavfi -i "aevalsrc=0:d=0.05" -c:a libfdk_aac -profile:a aac_he -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac
return new Uint8Array([
0x1,
0x40,
0x22,
0x80,
0xa3,
0x4e,
0xe6,
0x80,
0xba,
0x8,
0x0,
0x0,
0x0,
0x1c,
0x6,
0xf1,
0xc1,
0xa,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5e,
]);
} else if (channelCount === 2) {
// ffmpeg -y -f lavfi -i "aevalsrc=0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac
return new Uint8Array([
0x1,
0x40,
0x22,
0x80,
0xa3,
0x5e,
0xe6,
0x80,
0xba,
0x8,
0x0,
0x0,
0x0,
0x0,
0x95,
0x0,
0x6,
0xf1,
0xa1,
0xa,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5e,
]);
} else if (channelCount === 3) {
// ffmpeg -y -f lavfi -i "aevalsrc=0|0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac
return new Uint8Array([
0x1,
0x40,
0x22,
0x80,
0xa3,
0x5e,
0xe6,
0x80,
0xba,
0x8,
0x0,
0x0,
0x0,
0x0,
0x95,
0x0,
0x6,
0xf1,
0xa1,
0xa,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5a,
0x5e,
]);
}
break;
}
return undefined;
}
+791 -361
View File
File diff suppressed because it is too large Load Diff
+71 -63
View File
@@ -4,14 +4,7 @@ import type { HlsEventEmitter } from '../events';
import { Events } from '../events';
import { ErrorTypes, ErrorDetails } from '../errors';
import { logger } from '../utils/logger';
import {
InitSegmentData,
Remuxer,
RemuxerResult,
RemuxedMetadata,
RemuxedTrack,
RemuxedUserdata
} from '../types/remuxer';
import { InitSegmentData, Remuxer, RemuxerResult, RemuxedMetadata, RemuxedTrack, RemuxedUserdata } from '../types/remuxer';
import type { AvcSample, DemuxedAudioTrack, DemuxedAvcTrack, DemuxedTrack } from '../types/demuxer';
import type { TrackSet } from '../types/track';
import type { SourceBufferName } from '../types/buffer';
@@ -39,7 +32,7 @@ export default class MP4Remuxer implements Remuxer {
private isAudioContiguous: boolean = false;
private isVideoContiguous: boolean = false;
constructor (observer: HlsEventEmitter, config: HlsConfig, typeSupported, vendor = '') {
constructor(observer: HlsEventEmitter, config: HlsConfig, typeSupported, vendor = '') {
this.observer = observer;
this.config = config;
this.typeSupported = typeSupported;
@@ -57,30 +50,30 @@ export default class MP4Remuxer implements Remuxer {
requiresPositiveDts = (!!chromeVersion && chromeVersion < 75) || (!!safariWebkitVersion && safariWebkitVersion < 600);
}
destroy () {
}
destroy() {}
resetTimeStamp (defaultTimeStamp) {
resetTimeStamp(defaultTimeStamp) {
logger.log('[mp4-remuxer]: initPTS & initDTS reset reset');
this._initPTS = this._initDTS = defaultTimeStamp;
}
resetNextTimestamp () {
resetNextTimestamp() {
logger.log('[mp4-remuxer]: reset next timestamp');
this.isVideoContiguous = false;
this.isAudioContiguous = false;
}
resetInitSegment () {
resetInitSegment() {
logger.log('[mp4-remuxer]: ISGenerated flag reset');
this.ISGenerated = false;
}
getVideoStartPts (videoSamples) {
getVideoStartPts(videoSamples) {
let rolloverDetected = false;
const startPTS = videoSamples.reduce((minPTS, sample) => {
const delta = sample.pts - minPTS;
if (delta < -4294967296) { // 2^32, see PTSNormalize for reasoning, but we're hitting a rollover here, and we don't want that to impact the timeOffset calculation
if (delta < -4294967296) {
// 2^32, see PTSNormalize for reasoning, but we're hitting a rollover here, and we don't want that to impact the timeOffset calculation
rolloverDetected = true;
return PTSNormalize(minPTS, sample.pts);
} else if (delta > 0) {
@@ -95,7 +88,14 @@ export default class MP4Remuxer implements Remuxer {
return startPTS;
}
remux (audioTrack: DemuxedAudioTrack, videoTrack: DemuxedAvcTrack, id3Track: DemuxedTrack, textTrack: DemuxedTrack, timeOffset: number, accurateTimeOffset: boolean) : RemuxerResult {
remux(
audioTrack: DemuxedAudioTrack,
videoTrack: DemuxedAvcTrack,
id3Track: DemuxedTrack,
textTrack: DemuxedTrack,
timeOffset: number,
accurateTimeOffset: boolean
): RemuxerResult {
let video;
let audio;
let initSegment;
@@ -194,17 +194,17 @@ export default class MP4Remuxer implements Remuxer {
initSegment,
independent,
text,
id3
id3,
};
}
generateIS (audioTrack: DemuxedAudioTrack, videoTrack: DemuxedAvcTrack, timeOffset) : InitSegmentData | undefined {
generateIS(audioTrack: DemuxedAudioTrack, videoTrack: DemuxedAvcTrack, timeOffset): InitSegmentData | undefined {
logger.log('[mp4-remuxer]: generateIS', Object.assign({}, audioTrack), Object.assign({}, videoTrack), timeOffset);
const audioSamples = audioTrack.samples;
const videoSamples = videoTrack.samples;
const typeSupported = this.typeSupported;
const tracks: TrackSet = {};
const computePTSDTS = (!Number.isFinite(this._initPTS));
const computePTSDTS = !Number.isFinite(this._initPTS);
let container = 'audio/mp4';
let initPTS: number | undefined;
let initDTS: number | undefined;
@@ -222,10 +222,12 @@ export default class MP4Remuxer implements Remuxer {
audioTrack.timescale = audioTrack.samplerate;
logger.log(`[mp4-remuxer]: audio sampling rate : ${audioTrack.samplerate}`);
if (!audioTrack.isAAC) {
if (typeSupported.mpeg) { // Chrome and Safari
if (typeSupported.mpeg) {
// Chrome and Safari
container = 'audio/mpeg';
audioTrack.codec = '';
} else if (typeSupported.mp3) { // Firefox
} else if (typeSupported.mp3) {
// Firefox
audioTrack.codec = 'mp3';
}
}
@@ -235,8 +237,8 @@ export default class MP4Remuxer implements Remuxer {
codec: audioTrack.codec,
initSegment: !audioTrack.isAAC && typeSupported.mpeg ? new Uint8Array(0) : MP4.initSegment([audioTrack]),
metadata: {
channelCount: audioTrack.channelCount
}
channelCount: audioTrack.channelCount,
},
};
if (computePTSDTS) {
timescale = audioTrack.inputTimeScale;
@@ -256,8 +258,8 @@ export default class MP4Remuxer implements Remuxer {
initSegment: MP4.initSegment([videoTrack]),
metadata: {
width: videoTrack.width,
height: videoTrack.height
}
height: videoTrack.height,
},
};
if (computePTSDTS) {
timescale = videoTrack.inputTimeScale;
@@ -278,12 +280,12 @@ export default class MP4Remuxer implements Remuxer {
return {
tracks,
initPTS,
timescale
timescale,
};
}
}
remuxVideo (track: DemuxedAvcTrack, timeOffset: number, contiguous: boolean, audioTrackLength: number) : RemuxedTrack | undefined {
remuxVideo(track: DemuxedAvcTrack, timeOffset: number, contiguous: boolean, audioTrackLength: number): RemuxedTrack | undefined {
const timeScale: number = track.inputTimeScale;
const inputSamples: Array<AvcSample> = track.samples;
const outputSamples: Array<Mp4Sample> = [];
@@ -414,7 +416,7 @@ export default class MP4Remuxer implements Remuxer {
/* concatenate the video data and construct the mdat in place
(need 8 more bytes to fill length and mpdat type) */
const mdatSize = naluLen + (4 * nbNalu) + 8;
const mdatSize = naluLen + 4 * nbNalu + 8;
let mdat;
try {
mdat = new Uint8Array(mdatSize);
@@ -424,7 +426,7 @@ export default class MP4Remuxer implements Remuxer {
details: ErrorDetails.REMUX_ALLOC_ERROR,
fatal: false,
bytes: mdatSize,
reason: `fail allocating video mdat ${mdatSize}`
reason: `fail allocating video mdat ${mdatSize}`,
});
return;
}
@@ -494,9 +496,13 @@ export default class MP4Remuxer implements Remuxer {
// next AVC sample DTS should be equal to last sample DTS + last sample duration (in PES timescale)
this.nextAvcDts = nextAvcDts = lastDTS + mp4SampleDuration;
this.isVideoContiguous = true;
const moof = MP4.moof(track.sequenceNumber++, firstDTS, Object.assign({}, track, {
samples: outputSamples
}));
const moof = MP4.moof(
track.sequenceNumber++,
firstDTS,
Object.assign({}, track, {
samples: outputSamples,
})
);
const type: SourceBufferName = 'video';
const data = {
data1: moof,
@@ -504,12 +510,12 @@ export default class MP4Remuxer implements Remuxer {
startPTS: minPTS / timeScale,
endPTS: (maxPTS + mp4SampleDuration) / timeScale,
startDTS: firstDTS / timeScale,
endDTS: nextAvcDts as number / timeScale,
endDTS: (nextAvcDts as number) / timeScale,
type,
hasAudio: false,
hasVideo: true,
nb: outputSamples.length,
dropped: track.dropped
dropped: track.dropped,
};
track.samples = [];
@@ -520,7 +526,7 @@ export default class MP4Remuxer implements Remuxer {
return data;
}
remuxAudio (track: DemuxedAudioTrack, timeOffset: number, contiguous: boolean, accurateTimeOffset: boolean, videoTimeOffset?: number): RemuxedTrack | undefined {
remuxAudio(track: DemuxedAudioTrack, timeOffset: number, contiguous: boolean, accurateTimeOffset: boolean, videoTimeOffset?: number): RemuxedTrack | undefined {
const inputTimeScale: number = track.inputTimeScale;
const mp4timeScale: number = track.samplerate ? track.samplerate : inputTimeScale;
const scaleFactor: number = inputTimeScale / mp4timeScale;
@@ -545,10 +551,12 @@ export default class MP4Remuxer implements Remuxer {
// contiguous fragments are consecutive fragments from same quality level (same level, new SN = old SN + 1)
// this helps ensuring audio continuity
// and this also avoids audio glitches/cut when switching quality, or reporting wrong duration on first audio frame
this.isAudioContiguous = contiguous = contiguous || (inputSamples.length && nextAudioPts > 0 &&
((accurateTimeOffset && Math.abs(timeOffset - nextAudioPts / inputTimeScale) < 0.1) ||
Math.abs((inputSamples[0].pts - nextAudioPts - initPTS)) < 20 * inputSampleDuration)
) as boolean;
this.isAudioContiguous = contiguous =
contiguous ||
((inputSamples.length &&
nextAudioPts > 0 &&
((accurateTimeOffset && Math.abs(timeOffset - nextAudioPts / inputTimeScale) < 0.1) ||
Math.abs(inputSamples[0].pts - nextAudioPts - initPTS) < 20 * inputSampleDuration)) as boolean);
// compute normalized PTS
inputSamples.forEach(function (sample) {
@@ -586,12 +594,12 @@ export default class MP4Remuxer implements Remuxer {
if (track.isAAC) {
const maxAudioFramesDrift = this.config.maxAudioFramesDrift;
for (let i = 0, nextPts = nextAudioPts; i < inputSamples.length;) {
for (let i = 0, nextPts = nextAudioPts; i < inputSamples.length; ) {
// First, let's see how far off this frame is from where we expect it to be
const sample = inputSamples[i];
const pts = sample.pts;
const delta = pts - nextPts;
const duration = Math.abs(1000 * delta / inputTimeScale);
const duration = Math.abs((1000 * delta) / inputTimeScale);
// If we're overlapping by more than a duration, drop this sample
if (delta <= -maxAudioFramesDrift * inputSampleDuration) {
@@ -602,7 +610,7 @@ export default class MP4Remuxer implements Remuxer {
} else {
// When changing qualities we can't trust that audio has been appended up to nextAudioPts
// Warn about the overlap but do not drop samples as that can introduce buffer gaps
logger.warn(`Audio frame @ ${(pts / inputTimeScale).toFixed(3)}s overlaps nextAudioPts by ${Math.round(1000 * delta / inputTimeScale)} ms.`);
logger.warn(`Audio frame @ ${(pts / inputTimeScale).toFixed(3)}s overlaps nextAudioPts by ${Math.round((1000 * delta) / inputTimeScale)} ms.`);
nextPts = pts + inputSampleDuration;
i++;
}
@@ -617,7 +625,7 @@ export default class MP4Remuxer implements Remuxer {
// Adjust nextPts so that silent samples are aligned with media pts. This will prevent media samples from
// later being shifted if nextPts is based on timeOffset and delta is not a multiple of inputSampleDuration.
nextPts = pts - missing * inputSampleDuration;
logger.warn(`[mp4-remuxer]: Injecting ${missing} audio frame @ ${(nextPts / inputTimeScale).toFixed(3)}s due to ${Math.round(1000 * delta / inputTimeScale)} ms gap.`);
logger.warn(`[mp4-remuxer]: Injecting ${missing} audio frame @ ${(nextPts / inputTimeScale).toFixed(3)}s due to ${Math.round((1000 * delta) / inputTimeScale)} ms gap.`);
for (let j = 0; j < missing; j++) {
const newStamp = Math.max(nextPts as number, 0);
fillFrame = AAC.getSilentFrame(track.manifestCodec || track.codec, track.channelCount);
@@ -635,7 +643,7 @@ export default class MP4Remuxer implements Remuxer {
nextPts += inputSampleDuration;
i++;
} else {
// Otherwise, just adjust pts
// Otherwise, just adjust pts
sample.pts = sample.dts = nextPts;
nextPts += inputSampleDuration;
i++;
@@ -660,7 +668,7 @@ export default class MP4Remuxer implements Remuxer {
const prevSample = outputSamples[j - 1];
prevSample.duration = Math.round((pts - lastPTS) / scaleFactor);
} else {
const delta = Math.round(1000 * (pts - nextAudioPts) / inputTimeScale);
const delta = Math.round((1000 * (pts - nextAudioPts)) / inputTimeScale);
let numMissingFrames = 0;
// if fragment are contiguous, detect hole/overlapping between fragments
// contiguous fragments are consecutive fragments from same quality level (same level, new SN = old SN + 1)
@@ -679,7 +687,9 @@ export default class MP4Remuxer implements Remuxer {
// if we have frame overlap, overlapping for more than half a frame duraion
} else if (delta < -12) {
// drop overlapping audio frames... browser will deal with it
logger.log(`[mp4-remuxer]: drop overlapping AAC sample, expected/parsed/delta:${(nextAudioPts / inputTimeScale).toFixed(3)}s/${(pts / inputTimeScale).toFixed(3)}s/${(-delta)}ms`);
logger.log(
`[mp4-remuxer]: drop overlapping AAC sample, expected/parsed/delta:${(nextAudioPts / inputTimeScale).toFixed(3)}s/${(pts / inputTimeScale).toFixed(3)}s/${-delta}ms`
);
mdatSize -= unit.byteLength;
continue;
}
@@ -700,7 +710,7 @@ export default class MP4Remuxer implements Remuxer {
details: ErrorDetails.REMUX_ALLOC_ERROR,
fatal: false,
bytes: mdatSize,
reason: `fail allocating audio mdat ${mdatSize}`
reason: `fail allocating audio mdat ${mdatSize}`,
});
return;
}
@@ -745,9 +755,7 @@ export default class MP4Remuxer implements Remuxer {
this.nextAudioPts = nextAudioPts = lastPTS! + scaleFactor * lastSample.duration;
// Set the track samples from inputSamples to outputSamples before remuxing
const moof = rawMPEG
? new Uint8Array(0)
: MP4.moof(track.sequenceNumber++, firstPTS! / scaleFactor, Object.assign({}, track, { samples: outputSamples }));
const moof = rawMPEG ? new Uint8Array(0) : MP4.moof(track.sequenceNumber++, firstPTS! / scaleFactor, Object.assign({}, track, { samples: outputSamples }));
// Clear the track samples. This also clears the samples array in the demuxer, since the reference is shared
track.samples = [];
@@ -764,14 +772,14 @@ export default class MP4Remuxer implements Remuxer {
type,
hasAudio: true,
hasVideo: false,
nb: nbSamples
nb: nbSamples,
};
console.assert(mdat.length, 'MDAT length must not be zero');
return audioData;
}
remuxEmptyAudio (track: DemuxedAudioTrack, timeOffset: number, contiguous: boolean, videoData: Fragment) : RemuxedTrack | undefined {
remuxEmptyAudio(track: DemuxedAudioTrack, timeOffset: number, contiguous: boolean, videoData: Fragment): RemuxedTrack | undefined {
const inputTimeScale: number = track.inputTimeScale;
const mp4timeScale: number = track.samplerate ? track.samplerate : inputTimeScale;
const scaleFactor: number = inputTimeScale / mp4timeScale;
@@ -803,7 +811,7 @@ export default class MP4Remuxer implements Remuxer {
return this.remuxAudio(track, timeOffset, contiguous, false);
}
remuxID3 (track: DemuxedTrack, timeOffset: number) : RemuxedMetadata | undefined {
remuxID3(track: DemuxedTrack, timeOffset: number): RemuxedMetadata | undefined {
const length = track.samples.length;
if (!length) {
return;
@@ -821,11 +829,11 @@ export default class MP4Remuxer implements Remuxer {
const samples = track.samples;
track.samples = [];
return {
samples
samples,
};
}
remuxText (track: DemuxedTrack, timeOffset: number) : RemuxedUserdata | undefined {
remuxText(track: DemuxedTrack, timeOffset: number): RemuxedUserdata | undefined {
const length = track.samples.length;
if (!length) {
return;
@@ -843,12 +851,12 @@ export default class MP4Remuxer implements Remuxer {
const samples = track.samples;
track.samples = [];
return {
samples
samples,
};
}
}
function PTSNormalize (value: number, reference: number | null): number {
function PTSNormalize(value: number, reference: number | null): number {
let offset;
if (reference === null) {
return value;
@@ -871,7 +879,7 @@ function PTSNormalize (value: number, reference: number | null): number {
return value;
}
function findKeyframeIndex (samples: Array<AvcSample>) : number {
function findKeyframeIndex(samples: Array<AvcSample>): number {
for (let i = 0; i < samples.length; i++) {
if (samples[i].key) {
return i;
@@ -886,7 +894,7 @@ class Mp4Sample {
public cts: number;
public flags: Mp4SampleFlags;
constructor (isKeyframe: boolean, duration, size, cts) {
constructor(isKeyframe: boolean, duration, size, cts) {
this.duration = duration;
this.size = size;
this.cts = cts;
@@ -899,10 +907,10 @@ class Mp4SampleFlags {
public isDependedOn: 0 = 0;
public hasRedundancy: 0 = 0;
public degradPrio: 0 = 0;
public dependsOn: 1|2 = 1;
public isNonSync: 0|1 = 1;
public dependsOn: 1 | 2 = 1;
public isNonSync: 0 | 1 = 1;
constructor (isKeyframe) {
constructor(isKeyframe) {
this.dependsOn = isKeyframe ? 2 : 1;
this.isNonSync = isKeyframe ? 0 : 1;
}
+11 -12
View File
@@ -14,33 +14,32 @@ class PassThroughRemuxer implements Remuxer {
private initTracks?: TrackSet;
private lastEndDTS: number | null = null;
destroy () {
}
destroy() {}
resetTimeStamp (defaultInitPTS) {
resetTimeStamp(defaultInitPTS) {
this.initPTS = defaultInitPTS;
this.lastEndDTS = null;
}
resetNextTimestamp () {
resetNextTimestamp() {
this.lastEndDTS = null;
}
resetInitSegment (initSegment: Uint8Array, audioCodec: string | undefined, videoCodec: string | undefined) {
resetInitSegment(initSegment: Uint8Array, audioCodec: string | undefined, videoCodec: string | undefined) {
this.audioCodec = audioCodec;
this.videoCodec = videoCodec;
this.generateInitSegment(initSegment);
this.emitInitSegment = true;
}
generateInitSegment (initSegment: Uint8Array): void {
generateInitSegment(initSegment: Uint8Array): void {
let { audioCodec, videoCodec } = this;
if (!initSegment || !initSegment.byteLength) {
this.initTracks = undefined;
this.initData = undefined;
return;
}
const initData = this.initData = parseInitSegment(initSegment);
const initData = (this.initData = parseInitSegment(initSegment));
// default audio codec if nothing specified
// TODO : extract that from initsegment
@@ -58,7 +57,7 @@ class PassThroughRemuxer implements Remuxer {
container: 'video/mp4',
codec: audioCodec + ',' + videoCodec,
initSegment,
id: 'main'
id: 'main',
};
} else if (initData.audio) {
tracks.audio = { container: 'audio/mp4', codec: audioCodec, initSegment, id: 'audio' };
@@ -70,14 +69,14 @@ class PassThroughRemuxer implements Remuxer {
this.initTracks = tracks;
}
remux (audioTrack: DemuxedAudioTrack, videoTrack: PassthroughVideoTrack, id3Track: DemuxedTrack, textTrack: DemuxedTrack, timeOffset: number): RemuxerResult {
remux(audioTrack: DemuxedAudioTrack, videoTrack: PassthroughVideoTrack, id3Track: DemuxedTrack, textTrack: DemuxedTrack, timeOffset: number): RemuxerResult {
let { initPTS, lastEndDTS } = this;
const result: RemuxerResult = {
audio: undefined,
video: undefined,
text: textTrack,
id3: id3Track,
initSegment: undefined
initSegment: undefined,
};
// If we haven't yet set a lastEndDTS, or it was reset, set it to the provided timeOffset. We want to use the
@@ -96,7 +95,7 @@ class PassThroughRemuxer implements Remuxer {
const initSegment: InitSegmentData = {
initPTS: undefined,
timescale: 1
timescale: 1,
};
let initData = this.initData;
if (!initData || !initData.length) {
@@ -151,7 +150,7 @@ class PassThroughRemuxer implements Remuxer {
hasAudio,
hasVideo,
nb: 1,
dropped: 0
dropped: 0,
};
result.audio = track.type === 'audio' ? track : undefined;
+11 -11
View File
@@ -32,34 +32,34 @@ export default class TaskLoop {
private _tickInterval: number | null = null;
private _tickCallCount = 0;
constructor () {
constructor() {
this._boundTick = this.tick.bind(this);
}
public destroy () {
public destroy() {
this.onHandlerDestroying();
this.onHandlerDestroyed();
}
protected onHandlerDestroying () {
protected onHandlerDestroying() {
// clear all timers before unregistering from event bus
this.clearNextTick();
this.clearInterval();
}
protected onHandlerDestroyed () {}
protected onHandlerDestroyed() {}
/**
* @returns {boolean}
*/
public hasInterval (): boolean {
public hasInterval(): boolean {
return !!this._tickInterval;
}
/**
* @returns {boolean}
*/
public hasNextTick (): boolean {
public hasNextTick(): boolean {
return !!this._tickTimer;
}
@@ -67,7 +67,7 @@ export default class TaskLoop {
* @param {number} millis Interval time (ms)
* @returns {boolean} True when interval has been scheduled, false when already scheduled (no effect)
*/
public setInterval (millis: number): boolean {
public setInterval(millis: number): boolean {
if (!this._tickInterval) {
this._tickInterval = self.setInterval(this._boundTick, millis);
return true;
@@ -78,7 +78,7 @@ export default class TaskLoop {
/**
* @returns {boolean} True when interval was cleared, false when none was set (no effect)
*/
public clearInterval (): boolean {
public clearInterval(): boolean {
if (this._tickInterval) {
self.clearInterval(this._tickInterval);
this._tickInterval = null;
@@ -90,7 +90,7 @@ export default class TaskLoop {
/**
* @returns {boolean} True when timeout was cleared, false when none was set (no effect)
*/
public clearNextTick (): boolean {
public clearNextTick(): boolean {
if (this._tickTimer) {
self.clearTimeout(this._tickTimer);
this._tickTimer = null;
@@ -104,7 +104,7 @@ export default class TaskLoop {
* or in the next one (via setTimeout(,0)) in case it has already been called
* in this tick (in case this is a re-entrant call).
*/
public tick (): void {
public tick(): void {
this._tickCallCount++;
if (this._tickCallCount === 1) {
this.doTick();
@@ -123,5 +123,5 @@ export default class TaskLoop {
* For subclass to implement task logic
* @abstract
*/
protected doTick (): void {}
protected doTick(): void {}
}
+55 -55
View File
@@ -1,89 +1,89 @@
export interface Demuxer {
demux (data: Uint8Array, timeOffset: number, isSampleAes?: boolean) : DemuxerResult
demuxSampleAes (data: Uint8Array, decryptData: Uint8Array, timeOffset: number) : Promise<DemuxerResult>
flush(timeOffset?: number): DemuxerResult
destroy() : void
demux(data: Uint8Array, timeOffset: number, isSampleAes?: boolean): DemuxerResult;
demuxSampleAes(data: Uint8Array, decryptData: Uint8Array, timeOffset: number): Promise<DemuxerResult>;
flush(timeOffset?: number): DemuxerResult;
destroy(): void;
resetInitSegment(audioCodec: string | undefined, videoCodec: string | undefined, duration: number);
resetTimeStamp(defaultInitPTS?: number | null): void;
resetContiguity(): void;
}
export interface DemuxerResult {
audioTrack: DemuxedAudioTrack
avcTrack: DemuxedAvcTrack
id3Track: DemuxedTrack
textTrack: DemuxedTrack
audioTrack: DemuxedAudioTrack;
avcTrack: DemuxedAvcTrack;
id3Track: DemuxedTrack;
textTrack: DemuxedTrack;
}
export interface DemuxedTrack {
type: string
id: number
pid: number
inputTimeScale: number
sequenceNumber: number
samples: any
timescale?: number
container?: string
dropped: number
duration?: number
pesData?: ElementaryStreamData | null
codec?: string
type: string;
id: number;
pid: number;
inputTimeScale: number;
sequenceNumber: number;
samples: any;
timescale?: number;
container?: string;
dropped: number;
duration?: number;
pesData?: ElementaryStreamData | null;
codec?: string;
}
export interface DemuxedAudioTrack extends DemuxedTrack {
config?: Array<number>
samplerate?: number
isAAC?: boolean
channelCount?: number
manifestCodec?: string
config?: Array<number>;
samplerate?: number;
isAAC?: boolean;
channelCount?: number;
manifestCodec?: string;
}
export interface DemuxedVideoTrack extends DemuxedTrack {
width?: number
height?: number
pixelRatio?: number
audFound?: boolean
pps?: Array<number>
sps?: Array<number>
naluState?: number
width?: number;
height?: number;
pixelRatio?: number;
audFound?: boolean;
pps?: Array<number>;
sps?: Array<number>;
naluState?: number;
}
export interface DemuxedAvcTrack extends DemuxedVideoTrack {
samples: Array<AvcSample>
samples: Array<AvcSample>;
}
export interface PassthroughVideoTrack extends DemuxedVideoTrack {
samples: Uint8Array
samples: Uint8Array;
}
export interface DemuxedMetadataTrack extends DemuxedTrack {
samples: MetadataSample[]
samples: MetadataSample[];
}
export interface DemuxedUserdataTrack extends DemuxedTrack {
samples: UserdataSample[]
samples: UserdataSample[];
}
export interface MetadataSample {
pts: number,
dts: number,
len: number,
pts: number;
dts: number;
len: number;
data: Uint8Array;
}
export interface UserdataSample {
pts: number,
pts: number;
bytes: Uint8Array;
}
export interface AvcSample {
dts: number
pts: number
key: boolean
frame: boolean
units: Array<AvcSampleUnit>,
debug: string
length: number
dts: number;
pts: number;
key: boolean;
frame: boolean;
units: Array<AvcSampleUnit>;
debug: string;
length: number;
}
export interface AvcSampleUnit {
@@ -91,17 +91,17 @@ export interface AvcSampleUnit {
}
type AudioSample = {
unit: Uint8Array,
pts: number,
dts: number
}
unit: Uint8Array;
pts: number;
dts: number;
};
export type AppendedAudioFrame = {
sample: AudioSample,
length: number
sample: AudioSample;
length: number;
};
export interface ElementaryStreamData {
data: Array<Uint8Array>
size: number
data: Array<Uint8Array>;
size: number;
}
+158 -159
View File
@@ -16,7 +16,7 @@ import type AttrList from '../utils/attr-list';
import type { HlsListeners } from '../events';
export interface MediaAttachingData {
media: HTMLMediaElement
media: HTMLMediaElement;
}
export interface MediaAttachedData {
@@ -24,72 +24,72 @@ export interface MediaAttachedData {
}
export interface BufferCodecsData {
video?: Track
audio?: Track
video?: Track;
audio?: Track;
}
export interface BufferCreatedData {
tracks: TrackSet
tracks: TrackSet;
}
export interface BufferAppendingData {
type: SourceBufferName
data: Uint8Array
frag: Fragment
part: Part | null
chunkMeta: ChunkMetadata
type: SourceBufferName;
data: Uint8Array;
frag: Fragment;
part: Part | null;
chunkMeta: ChunkMetadata;
}
export interface BufferAppendedData {
chunkMeta: ChunkMetadata
frag: Fragment
part: Part | null
parent: PlaylistLevelType
chunkMeta: ChunkMetadata;
frag: Fragment;
part: Part | null;
parent: PlaylistLevelType;
timeRanges: {
audio?: TimeRanges
video?: TimeRanges
audiovideo?: TimeRanges
}
audio?: TimeRanges;
video?: TimeRanges;
audiovideo?: TimeRanges;
};
}
export interface BufferEOSData {
type: SourceBufferName
type: SourceBufferName;
}
export interface BufferFlushingData {
startOffset: number
endOffset: number
type: SourceBufferName | null
startOffset: number;
endOffset: number;
type: SourceBufferName | null;
}
export interface BufferFlushedData {
type: SourceBufferName
type: SourceBufferName;
}
export interface ManifestLoadingData {
url: string
url: string;
}
export interface ManifestLoadedData {
audioTracks: MediaPlaylist[]
captions?: MediaPlaylist[]
levels: LevelParsed[]
networkDetails: any
sessionData: Record<string, AttrList> | null
stats: LoaderStats
subtitles?: MediaPlaylist[]
url: string
audioTracks: MediaPlaylist[];
captions?: MediaPlaylist[];
levels: LevelParsed[];
networkDetails: any;
sessionData: Record<string, AttrList> | null;
stats: LoaderStats;
subtitles?: MediaPlaylist[];
url: string;
}
export interface ManifestParsedData {
levels: Level[]
audioTracks: MediaPlaylist[]
subtitleTracks: MediaPlaylist[]
firstLevel: number
stats: LoaderStats
audio: boolean
video: boolean
altAudio: boolean
levels: Level[];
audioTracks: MediaPlaylist[];
subtitleTracks: MediaPlaylist[];
firstLevel: number;
stats: LoaderStats;
audio: boolean;
video: boolean;
altAudio: boolean;
}
export interface LevelSwitchingData extends Omit<Level, '_urlId'> {
@@ -97,90 +97,90 @@ export interface LevelSwitchingData extends Omit<Level, '_urlId'> {
}
export interface LevelSwitchedData {
level: number
level: number;
}
export interface TrackLoadingData {
id: number
groupId: string
url: string
deliveryDirectives: HlsUrlParameters | null
id: number;
groupId: string;
url: string;
deliveryDirectives: HlsUrlParameters | null;
}
export interface LevelLoadingData {
id: number
level: number
url: string
deliveryDirectives: HlsUrlParameters | null
id: number;
level: number;
url: string;
deliveryDirectives: HlsUrlParameters | null;
}
export interface TrackLoadedData {
details: LevelDetails
id: number
groupId: string
networkDetails: any
stats: LoaderStats
deliveryDirectives: HlsUrlParameters | null
details: LevelDetails;
id: number;
groupId: string;
networkDetails: any;
stats: LoaderStats;
deliveryDirectives: HlsUrlParameters | null;
}
export interface LevelLoadedData {
details: LevelDetails
id: number
level: number
networkDetails: any
stats: LoaderStats
deliveryDirectives: HlsUrlParameters | null
details: LevelDetails;
id: number;
level: number;
networkDetails: any;
stats: LoaderStats;
deliveryDirectives: HlsUrlParameters | null;
}
export interface LevelUpdatedData {
details: LevelDetails
level: number
details: LevelDetails;
level: number;
}
export interface LevelPTSUpdatedData {
details: LevelDetails,
level: Level,
drift: number,
type: string,
start: number,
end: number
details: LevelDetails;
level: Level;
drift: number;
type: string;
start: number;
end: number;
}
export interface AudioTrackSwitchingData {
url: string
type: MediaPlaylistType | 'main'
id: number
url: string;
type: MediaPlaylistType | 'main';
id: number;
}
export interface AudioTrackSwitchedData {
id: number
id: number;
}
export interface AudioTrackLoadedData extends TrackLoadedData {}
export interface AudioTracksUpdatedData {
audioTracks: MediaPlaylist[]
audioTracks: MediaPlaylist[];
}
export interface SubtitleTracksUpdatedData {
subtitleTracks: MediaPlaylist[]
subtitleTracks: MediaPlaylist[];
}
export interface SubtitleTrackSwitchData {
url?: string
type?: MediaPlaylistType | 'main'
id: number
url?: string;
type?: MediaPlaylistType | 'main';
id: number;
}
export interface SubtitleTrackLoadedData extends TrackLoadedData {}
export interface TrackSwitchedData {
id: number
id: number;
}
export interface SubtitleFragProcessed {
success: boolean,
frag: Fragment
success: boolean;
frag: Fragment;
}
export interface FragChangedData {
@@ -188,146 +188,145 @@ export interface FragChangedData {
}
export interface FPSDropData {
currentDropped: number
currentDecoded: number
totalDroppedFrames: number
currentDropped: number;
currentDecoded: number;
totalDroppedFrames: number;
}
export interface FPSDropLevelCappingData {
droppedLevel: number
level: number
droppedLevel: number;
level: number;
}
export interface ErrorData {
type: ErrorTypes
details: ErrorDetails
fatal: boolean
buffer?: number
bytes?: number
context?: PlaylistLoaderContext
error?: Error
event?: keyof HlsListeners | 'demuxerWorker'
frag?: Fragment
level?: number | undefined
levelRetry?: boolean
loader?: Loader<LoaderContext>
networkDetails?: any
mimeType?: string
reason?: string
response?: LoaderResponse
url?: string
parent?: PlaylistLevelType
err?: { // comes from transmuxer interface
type: ErrorTypes;
details: ErrorDetails;
fatal: boolean;
buffer?: number;
bytes?: number;
context?: PlaylistLoaderContext;
error?: Error;
event?: keyof HlsListeners | 'demuxerWorker';
frag?: Fragment;
level?: number | undefined;
levelRetry?: boolean;
loader?: Loader<LoaderContext>;
networkDetails?: any;
mimeType?: string;
reason?: string;
response?: LoaderResponse;
url?: string;
parent?: PlaylistLevelType;
err?: {
// comes from transmuxer interface
message: string;
}
};
}
export interface SubtitleFragProcessedData {
success: boolean
frag: Fragment
error?: Error
success: boolean;
frag: Fragment;
error?: Error;
}
export interface CuesParsedData {
type: 'captions' | 'subtitles',
cues: any,
track: string
type: 'captions' | 'subtitles';
cues: any;
track: string;
}
interface NonNativeTextTrack {
_id?: string
label: any
kind: string
default: boolean
closedCaptions?: MediaPlaylist
subtitleTrack?: MediaPlaylist
_id?: string;
label: any;
kind: string;
default: boolean;
closedCaptions?: MediaPlaylist;
subtitleTrack?: MediaPlaylist;
}
export interface NonNativeTextTracksData {
tracks: Array<NonNativeTextTrack>
tracks: Array<NonNativeTextTrack>;
}
export interface InitPTSFoundData {
id: string
frag: Fragment
initPTS: number
timescale: number
id: string;
frag: Fragment;
initPTS: number;
timescale: number;
}
export interface FragLoadingData {
frag: Fragment
part?: Part
targetBufferTime: number | null
frag: Fragment;
part?: Part;
targetBufferTime: number | null;
}
export interface FragLoadEmergencyAbortedData {
frag: Fragment
part: Part | null
stats: LoaderStats
frag: Fragment;
part: Part | null;
stats: LoaderStats;
}
export interface FragLoadedData {
frag: Fragment
part: Part | null
payload: ArrayBuffer
networkDetails: unknown
frag: Fragment;
part: Part | null;
payload: ArrayBuffer;
networkDetails: unknown;
}
export interface PartsLoadedData {
frag: Fragment
part: Part | null
partsLoaded?: FragLoadedData[]
frag: Fragment;
part: Part | null;
partsLoaded?: FragLoadedData[];
}
export interface FragDecryptedData {
frag: Fragment
payload: ArrayBuffer
frag: Fragment;
payload: ArrayBuffer;
stats: {
tstart: number
tdecrypt: number
}
tstart: number;
tdecrypt: number;
};
}
export interface FragParsingInitSegmentData {
}
export interface FragParsingInitSegmentData {}
export interface FragParsingUserdataData {
id: string,
frag: Fragment,
samples: UserdataSample[]
id: string;
frag: Fragment;
samples: UserdataSample[];
}
export interface FragParsingMetadataData {
id: string,
frag: Fragment,
samples: MetadataSample[]
id: string;
frag: Fragment;
samples: MetadataSample[];
}
export interface FragParsedData {
frag: Fragment,
part: Part | null
frag: Fragment;
part: Part | null;
}
export interface FragBufferedData {
stats: LoadStats
frag: Fragment,
part: Part | null
id: string
stats: LoadStats;
frag: Fragment;
part: Part | null;
id: string;
}
export interface LevelsUpdatedData {
levels: Array<Level>
levels: Array<Level>;
}
export interface KeyLoadingData {
frag: Fragment
frag: Fragment;
}
export interface KeyLoadedData {
frag: Fragment
frag: Fragment;
}
export interface LiveBackBufferData {
bufferEnd: number
bufferEnd: number;
}
+10 -10
View File
@@ -6,20 +6,20 @@ import type { SourceBufferName } from './buffer';
import type { FragLoadedData } from './events';
export interface FragmentEntity {
body: Fragment,
part: Part | null
loaded: FragLoadedData | null,
backtrack: FragLoadedData | null,
buffered: boolean,
range: { [key in SourceBufferName]: FragmentBufferedRange }
body: Fragment;
part: Part | null;
loaded: FragLoadedData | null;
backtrack: FragLoadedData | null;
buffered: boolean;
range: { [key in SourceBufferName]: FragmentBufferedRange };
}
export interface FragmentTimeRange {
startPTS: number
endPTS: number
startPTS: number;
endPTS: number;
}
export interface FragmentBufferedRange {
time: Array<FragmentTimeRange>
partial: boolean
time: Array<FragmentTimeRange>;
partial: boolean;
}
+9 -6
View File
@@ -1,8 +1,11 @@
export interface StringMap { [key: string]: any; }
export interface StringMap {
[key: string]: any;
}
/**
* Make specific properties in T required
*/
export type RequiredProperties<T, K extends keyof T> = T & {
[P in K]-?: T[P];
};
* Make specific properties in T required
*/
export type RequiredProperties<T, K extends keyof T> = T &
{
[P in K]-?: T[P];
};
+40 -40
View File
@@ -2,48 +2,48 @@ import LevelDetails from '../loader/level-details';
import AttrList from '../utils/attr-list';
export interface LevelParsed {
attrs: LevelAttributes
audioCodec?: string
bitrate: number
details?: LevelDetails
height?: number
id?: number
level?: number
name: string
textCodec?: string
unknownCodecs?: string[]
url: string
videoCodec?: string
width?: number
attrs: LevelAttributes;
audioCodec?: string;
bitrate: number;
details?: LevelDetails;
height?: number;
id?: number;
level?: number;
name: string;
textCodec?: string;
unknownCodecs?: string[];
url: string;
videoCodec?: string;
width?: number;
}
export interface LevelAttributes extends AttrList {
AUDIO?: string
AUTOSELECT?: string
'AVERAGE-BANDWIDTH'?: string
BANDWIDTH?: string
BYTERANGE?: string
'CLOSED-CAPTIONS'?: string
CODECS?: string
DEFAULT?: string
FORCED?: string
'FRAME-RATE'?: string
LANGUAGE?: string
NAME?: string
'PROGRAM-ID'?: string
RESOLUTION?: string
SUBTITLES?: string
TYPE?: string
URI?: string
AUDIO?: string;
AUTOSELECT?: string;
'AVERAGE-BANDWIDTH'?: string;
BANDWIDTH?: string;
BYTERANGE?: string;
'CLOSED-CAPTIONS'?: string;
CODECS?: string;
DEFAULT?: string;
FORCED?: string;
'FRAME-RATE'?: string;
LANGUAGE?: string;
NAME?: string;
'PROGRAM-ID'?: string;
RESOLUTION?: string;
SUBTITLES?: string;
TYPE?: string;
URI?: string;
}
export enum HlsSkip {
No = '',
Yes = 'YES',
v2 = 'v2'
v2 = 'v2',
}
export function getSkipValue (details: LevelDetails, msn: number): HlsSkip {
export function getSkipValue(details: LevelDetails, msn: number): HlsSkip {
const { canSkipUntil, canSkipDateRanges, endSN } = details;
const snChangeGoal = msn - endSN;
if (canSkipUntil && snChangeGoal < canSkipUntil) {
@@ -60,13 +60,13 @@ export class HlsUrlParameters {
part?: number;
skip?: HlsSkip;
constructor (msn: number, part?: number, skip?: HlsSkip) {
constructor(msn: number, part?: number, skip?: HlsSkip) {
this.msn = msn;
this.part = part;
this.skip = skip;
}
addDirectives (uri: string): string | never {
addDirectives(uri: string): string | never {
const url: URL = new self.URL(uri);
const searchParams: URLSearchParams = url.searchParams;
searchParams.set('_HLS_msn', this.msn.toString());
@@ -92,7 +92,7 @@ export class Level {
public height: number;
public id: number;
public loadError: number = 0;
public loaded?: { bytes: number, duration: number };
public loaded?: { bytes: number; duration: number };
public name: string | undefined;
public realBitrate: number = 0;
public textGroupIds?: string[];
@@ -102,7 +102,7 @@ export class Level {
public unknownCodecs: string[] | undefined;
private _urlId: number = 0;
constructor (data: LevelParsed) {
constructor(data: LevelParsed) {
this.url = [data.url];
this.attrs = data.attrs;
this.bitrate = data.bitrate;
@@ -116,19 +116,19 @@ export class Level {
this.unknownCodecs = data.unknownCodecs;
}
get maxBitrate (): number {
get maxBitrate(): number {
return Math.max(this.realBitrate, this.bitrate);
}
get uri (): string {
get uri(): string {
return this.url[this._urlId] || '';
}
get urlId (): number {
get urlId(): number {
return this._urlId;
}
set urlId (value: number) {
set urlId(value: number) {
const newValue = value % this.url.length;
if (this._urlId !== newValue) {
this.details = undefined;
+50 -72
View File
@@ -5,40 +5,40 @@ import type { HlsUrlParameters } from './level';
export interface LoaderContext {
// target URL
url: string
url: string;
// loader response type (arraybuffer or default response type for playlist)
responseType: string
responseType: string;
// start byte range offset
rangeStart?: number
rangeStart?: number;
// end byte range offset
rangeEnd?: number
rangeEnd?: number;
// true if onProgress should report partial chunk of loaded content
progressData?: boolean
progressData?: boolean;
}
export interface FragmentLoaderContext extends LoaderContext {
frag: Fragment,
part: Part | null
frag: Fragment;
part: Part | null;
}
export interface LoaderConfiguration {
// Max number of load retries
maxRetry: number
maxRetry: number;
// Timeout after which `onTimeOut` callback will be triggered
// (if loading is still not finished after that delay)
timeout: number
timeout: number;
// Delay between an I/O error and following connection retry (ms).
// This to avoid spamming the server
retryDelay: number
retryDelay: number;
// max connection retry delay (ms)
maxRetryDelay: number
maxRetryDelay: number;
// When streaming progressively, this is the minimum chunk size required to emit a PROGRESS event
highWaterMark: number
highWaterMark: number;
}
export interface LoaderResponse {
url: string,
data: string | ArrayBuffer
url: string;
data: string | ArrayBuffer;
}
export interface LoaderStats {
@@ -67,92 +67,70 @@ export interface HlsProgressivePerformanceTiming extends HlsPerformanceTiming {
first: number;
}
type LoaderOnSuccess < T extends LoaderContext > = (
response: LoaderResponse,
stats: LoaderStats,
type LoaderOnSuccess<T extends LoaderContext> = (response: LoaderResponse, stats: LoaderStats, context: T, networkDetails: any) => void;
export type LoaderOnProgress<T extends LoaderContext> = (stats: LoaderStats, context: T, data: string | ArrayBuffer, networkDetails: any) => void;
type LoaderOnError<T extends LoaderContext> = (
error: {
// error status code
code: number;
// error description
text: string;
},
context: T,
networkDetails: any
) => void;
export type LoaderOnProgress < T extends LoaderContext > = (
stats: LoaderStats,
context: T,
data: string | ArrayBuffer,
networkDetails: any,
) => void;
type LoaderOnTimeout<T extends LoaderContext> = (stats: LoaderStats, context: T, networkDetails: any) => void;
type LoaderOnError < T extends LoaderContext > = (
error: {
// error status code
code: number,
// error description
text: string,
},
context: T,
networkDetails: any,
) => void;
type LoaderOnAbort<T extends LoaderContext> = (stats: LoaderStats, context: T, networkDetails: any) => void;
type LoaderOnTimeout < T extends LoaderContext > = (
stats: LoaderStats,
context: T,
networkDetails: any,
) => void;
type LoaderOnAbort < T extends LoaderContext > = (
stats: LoaderStats,
context: T,
networkDetails: any,
) => void;
export interface LoaderCallbacks<T extends LoaderContext>{
onSuccess: LoaderOnSuccess<T>,
onError: LoaderOnError<T>,
onTimeout: LoaderOnTimeout<T>,
onAbort?: LoaderOnAbort<T>,
onProgress?: LoaderOnProgress<T>,
export interface LoaderCallbacks<T extends LoaderContext> {
onSuccess: LoaderOnSuccess<T>;
onError: LoaderOnError<T>;
onTimeout: LoaderOnTimeout<T>;
onAbort?: LoaderOnAbort<T>;
onProgress?: LoaderOnProgress<T>;
}
export interface Loader<T extends LoaderContext> {
destroy(): void
abort(): void
load(
context: LoaderContext,
config: LoaderConfiguration,
callbacks: LoaderCallbacks<T>,
): void
getResponseHeader(name:string): string | null
context: T
loader: any
stats: LoaderStats
destroy(): void;
abort(): void;
load(context: LoaderContext, config: LoaderConfiguration, callbacks: LoaderCallbacks<T>): void;
getResponseHeader(name: string): string | null;
context: T;
loader: any;
stats: LoaderStats;
}
export enum PlaylistContextType {
MANIFEST = 'manifest',
LEVEL = 'level',
AUDIO_TRACK = 'audioTrack',
SUBTITLE_TRACK= 'subtitleTrack'
SUBTITLE_TRACK = 'subtitleTrack',
}
export enum PlaylistLevelType {
MAIN = 'main',
AUDIO = 'audio',
SUBTITLE = 'subtitle'
SUBTITLE = 'subtitle',
}
export interface PlaylistLoaderContext extends LoaderContext {
loader?: Loader<PlaylistLoaderContext>
loader?: Loader<PlaylistLoaderContext>;
type: PlaylistContextType
type: PlaylistContextType;
// the level index to load
level: number | null
level: number | null;
// level or track id from LevelLoadingData / TrackLoadingData
id: number | null
id: number | null;
// track group id
groupId: string | null
groupId: string | null;
// defines if the loader is handling a sidx request for the playlist
isSidxRequest?: boolean
isSidxRequest?: boolean;
// internal representation of a parsed m3u8 level playlist
levelDetails?: LevelDetails,
levelDetails?: LevelDetails;
// Blocking playlist request delivery directives (or null id none were added to playlist url
deliveryDirectives: HlsUrlParameters | null
deliveryDirectives: HlsUrlParameters | null;
}
+38 -42
View File
@@ -1,62 +1,58 @@
import type { TrackSet } from './track';
import {
DemuxedAudioTrack,
DemuxedTrack, DemuxedVideoTrack,
MetadataSample,
UserdataSample
} from './demuxer';
import { DemuxedAudioTrack, DemuxedTrack, DemuxedVideoTrack, MetadataSample, UserdataSample } from './demuxer';
import type { SourceBufferName } from './buffer';
export interface Remuxer {
remux(audioTrack: DemuxedAudioTrack,
videoTrack: DemuxedVideoTrack,
id3Track: DemuxedTrack,
textTrack: DemuxedTrack,
timeOffset: number,
accurateTimeOffset: boolean
): RemuxerResult
resetInitSegment(initSegment: Uint8Array, audioCodec: string | undefined, videoCodec: string | undefined): void
resetTimeStamp(defaultInitPTS): void
resetNextTimestamp() : void
destroy() : void
remux(
audioTrack: DemuxedAudioTrack,
videoTrack: DemuxedVideoTrack,
id3Track: DemuxedTrack,
textTrack: DemuxedTrack,
timeOffset: number,
accurateTimeOffset: boolean
): RemuxerResult;
resetInitSegment(initSegment: Uint8Array, audioCodec: string | undefined, videoCodec: string | undefined): void;
resetTimeStamp(defaultInitPTS): void;
resetNextTimestamp(): void;
destroy(): void;
}
export interface RemuxedTrack {
data1: Uint8Array
data2?: Uint8Array
startPTS: number
endPTS: number
startDTS: number
endDTS: number
type: SourceBufferName
hasAudio: boolean
hasVideo: boolean
independent?: boolean
nb: number
transferredData1?: ArrayBuffer
transferredData2?: ArrayBuffer
dropped?: number
data1: Uint8Array;
data2?: Uint8Array;
startPTS: number;
endPTS: number;
startDTS: number;
endDTS: number;
type: SourceBufferName;
hasAudio: boolean;
hasVideo: boolean;
independent?: boolean;
nb: number;
transferredData1?: ArrayBuffer;
transferredData2?: ArrayBuffer;
dropped?: number;
}
export interface RemuxedMetadata {
samples: MetadataSample[]
samples: MetadataSample[];
}
export interface RemuxedUserdata {
samples: UserdataSample[]
samples: UserdataSample[];
}
export interface RemuxerResult {
audio?: RemuxedTrack
video?: RemuxedTrack
text?: RemuxedUserdata
id3?: RemuxedMetadata
initSegment?: InitSegmentData
independent?: boolean
audio?: RemuxedTrack;
video?: RemuxedTrack;
text?: RemuxedUserdata;
id3?: RemuxedMetadata;
initSegment?: InitSegmentData;
independent?: boolean;
}
export interface InitSegmentData {
tracks?: TrackSet
initPTS: number | undefined
timescale: number | undefined
tracks?: TrackSet;
initPTS: number | undefined;
timescale: number | undefined;
}
+7 -7
View File
@@ -1,15 +1,15 @@
export interface TrackSet {
audio?: Track
video?: Track
audiovideo?: Track
audio?: Track;
video?: Track;
audiovideo?: Track;
}
export interface Track {
id: 'audio' | 'main'
buffer?: SourceBuffer // eslint-disable-line no-restricted-globals
id: 'audio' | 'main';
buffer?: SourceBuffer; // eslint-disable-line no-restricted-globals
container: string;
codec?: string;
initSegment?: Uint8Array
initSegment?: Uint8Array;
levelCodec?: string;
metadata?: any
metadata?: any;
}
+5 -5
View File
@@ -3,8 +3,8 @@ import type { HlsChunkPerformanceTiming } from './loader';
import type { SourceBufferName } from './buffer';
export interface TransmuxerResult {
remuxResult: RemuxerResult
chunkMeta: ChunkMetadata
remuxResult: RemuxerResult;
chunkMeta: ChunkMetadata;
}
export class ChunkMetadata {
@@ -18,10 +18,10 @@ export class ChunkMetadata {
public readonly buffering: { [key in SourceBufferName]: HlsChunkPerformanceTiming } = {
audio: getNewPerformanceTiming(),
video: getNewPerformanceTiming(),
audiovideo: getNewPerformanceTiming()
audiovideo: getNewPerformanceTiming(),
};
constructor (level: number, sn: number, id: number, size = 0, part = -1, partial = false) {
constructor(level: number, sn: number, id: number, size = 0, part = -1, partial = false) {
this.level = level;
this.sn = sn;
this.id = id;
@@ -31,6 +31,6 @@ export class ChunkMetadata {
}
}
function getNewPerformanceTiming (): HlsChunkPerformanceTiming {
function getNewPerformanceTiming(): HlsChunkPerformanceTiming {
return { start: 0, executeStart: 0, executeEnd: 0, end: 0 };
}
+1 -4
View File
@@ -1,4 +1 @@
export type Tail<T extends any[]> =
((...t: T) => any) extends ((_: any, ...tail: infer U) => any)
? U
: [];
export type Tail<T extends any[]> = ((...t: T) => any) extends (_: any, ...tail: infer U) => any ? U : [];
+21 -18
View File
@@ -5,9 +5,9 @@ const ATTR_LIST_REGEX = /\s*(.+?)\s*=((?:\".*?\")|.*?)(?:,|$)/g; // eslint-disab
// adapted from https://github.com/kanongil/node-m3u8parse/blob/master/attrlist.js
class AttrList {
[key: string]: any
[key: string]: any;
constructor (attrs: string | StringMap) {
constructor(attrs: string | StringMap) {
if (typeof attrs === 'string') {
attrs = AttrList.parseAttrList(attrs);
}
@@ -19,7 +19,7 @@ class AttrList {
}
}
decimalInteger (attrName: string): number {
decimalInteger(attrName: string): number {
const intValue = parseInt(this[attrName], 10);
if (intValue > Number.MAX_SAFE_INTEGER) {
return Infinity;
@@ -28,10 +28,10 @@ class AttrList {
return intValue;
}
hexadecimalInteger (attrName: string) {
hexadecimalInteger(attrName: string) {
if (this[attrName]) {
let stringValue = (this[attrName] || '0x').slice(2);
stringValue = ((stringValue.length & 1) ? '0' : '') + stringValue;
stringValue = (stringValue.length & 1 ? '0' : '') + stringValue;
const value = new Uint8Array(stringValue.length / 2);
for (let i = 0; i < stringValue.length / 2; i++) {
@@ -44,7 +44,7 @@ class AttrList {
}
}
hexadecimalIntegerAsNumber (attrName: string): number {
hexadecimalIntegerAsNumber(attrName: string): number {
const intValue = parseInt(this[attrName], 16);
if (intValue > Number.MAX_SAFE_INTEGER) {
return Infinity;
@@ -53,27 +53,31 @@ class AttrList {
return intValue;
}
decimalFloatingPoint (attrName: string): number {
decimalFloatingPoint(attrName: string): number {
return parseFloat(this[attrName]);
}
optionalFloat (attrName: string, defaultValue: number): number {
optionalFloat(attrName: string, defaultValue: number): number {
const value = this[attrName];
return value ? parseFloat(value) : defaultValue;
}
enumeratedString (attrName: string): string | undefined {
enumeratedString(attrName: string): string | undefined {
return this[attrName];
}
bool (attrName: string): boolean {
bool(attrName: string): boolean {
return this[attrName] === 'YES';
}
decimalResolution (attrName: string): {
width: number,
height: number
} | undefined {
decimalResolution(
attrName: string
):
| {
width: number;
height: number;
}
| undefined {
const res = DECIMAL_RESOLUTION_REGEX.exec(this[attrName]);
if (res === null) {
return undefined;
@@ -81,11 +85,11 @@ class AttrList {
return {
width: parseInt(res[1], 10),
height: parseInt(res[2], 10)
height: parseInt(res[2], 10),
};
}
static parseAttrList (input: string): StringMap {
static parseAttrList(input: string): StringMap {
let match;
const attrs = {};
const quote = '"';
@@ -93,8 +97,7 @@ class AttrList {
while ((match = ATTR_LIST_REGEX.exec(input)) !== null) {
let value = match[2];
if (value.indexOf(quote) === 0 &&
value.lastIndexOf(quote) === (value.length - 1)) {
if (value.indexOf(quote) === 0 && value.lastIndexOf(quote) === value.length - 1) {
value = value.slice(1, -1);
}

Some files were not shown because too many files have changed in this diff Show More