mirror of
https://github.com/video-dev/hls.js.git
synced 2026-05-17 13:30:38 +00:00
run 'npm run prettier'
This commit is contained in:
+2
-2
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,7 +1,6 @@
|
||||
---
|
||||
name: 'Question '
|
||||
about: Need some help?
|
||||
|
||||
---
|
||||
|
||||
**What do you want to do with Hls.js?**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+10
-12
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
[](https://saucelabs.com/u/robwalch)
|
||||
[](https://saucelabs.com)
|
||||
[](https://saucelabs.com)
|
||||
|
||||
+2
-10
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 & 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
File diff suppressed because it is too large
Load Diff
+62
-52
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+238
-239
@@ -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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -55,5 +55,5 @@ module.exports = {
|
||||
throw new Error('Invalid version.');
|
||||
}
|
||||
return match[1] || 'latest';
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
+114
-113
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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] || [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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,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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+71
-63
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user