diff --git a/.github/workflows/github-release.yml b/.github/workflows/release.yml similarity index 84% rename from .github/workflows/github-release.yml rename to .github/workflows/release.yml index 76c9d94c3..8e457c629 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: github-release +name: release on: push: tags: @@ -7,7 +7,7 @@ on: # match semver pre-releases - "v[0-9]+.[0-9]+.[0-9]+-*" jobs: - github-release: + release: env: NETLIFY_BASE: 'videojs-http-streaming.netlify.app' runs-on: ubuntu-latest @@ -29,12 +29,17 @@ jobs: with: node-version: '${{steps.nvm.outputs.NVMRC}}' cache: npm + # this line is required for the setup-node action to be able to run the npm publish below. + registry-url: 'https://registry.npmjs.org' - name: npm install run: npm i --prefer-offline --no-audit - - name: build - run: npm run build-prod --if-present + # publish runs build for us via a prepublishOnly script + - name: npm release + run: npm publish --tag next + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Check if this is a pre-release run: echo ::set-output name=IS_PRE_RELEASE::$(npx -p not-prerelease is-prerelease && echo "true" || echo "false") @@ -67,3 +72,5 @@ jobs: files: | dist/**/*.js dist/**/*.css + discussion_category_name: Releases + diff --git a/CHANGELOG.md b/CHANGELOG.md index e49d3dd1c..38da60ee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,33 @@ + +# [3.1.0](https://github.com/videojs/http-streaming/compare/v3.0.2...v3.1.0) (2023-03-07) + +### Features + +* add fmp4 emsg ID3 support ([#1370](https://github.com/videojs/http-streaming/issues/1370)) ([906f29e](https://github.com/videojs/http-streaming/commit/906f29e)) + +### Chores + +* npm publish for release workflow ([#1376](https://github.com/videojs/http-streaming/issues/1376)) ([e5b4bf6](https://github.com/videojs/http-streaming/commit/e5b4bf6)) + + +## [3.0.2](https://github.com/videojs/http-streaming/compare/v3.0.1...v3.0.2) (2023-02-27) + +### Bug Fixes + +* CMAF HLS. Source buffer change type is called with wrong codecs sometimes when append segment without init data because of a race condition. ([#1375](https://github.com/videojs/http-streaming/issues/1375)) ([7c3e08e](https://github.com/videojs/http-streaming/commit/7c3e08e)) + +### Chores + +* **changelog:** add missing bug fix ([#1362](https://github.com/videojs/http-streaming/issues/1362)) ([343f682](https://github.com/videojs/http-streaming/commit/343f682)) +* update mux.js ([#1372](https://github.com/videojs/http-streaming/issues/1372)) ([1bd22c9](https://github.com/videojs/http-streaming/commit/1bd22c9)) + ## [3.0.1](https://github.com/videojs/http-streaming/compare/v3.0.0...v3.0.1) (2023-01-24) ### Bug Fixes * Linear DASH multiperiod label issue ([#1352](https://github.com/videojs/http-streaming/issues/1352)) ([d7e8713](https://github.com/videojs/http-streaming/commit/d7e8713)) +* In-manifest VTT iOS MSE issue ([#1360](https://github.com/videojs/http-streaming/issues/1360)) ([6ba70e0](https://github.com/videojs/http-streaming/commit/6ba70e0)) # [3.0.0](https://github.com/videojs/http-streaming/compare/v2.14.2...v3.0.0) (2022-11-21) diff --git a/COLLABORATOR_GUIDE.md b/COLLABORATOR_GUIDE.md index 3fc89ddf9..4aa46470e 100644 --- a/COLLABORATOR_GUIDE.md +++ b/COLLABORATOR_GUIDE.md @@ -59,7 +59,7 @@ Install dependencies for the project. npm install ``` -Then, it's mostly a standard npm package release process with running `npm version`, followed by an `npm publish`. +Update version. ```sh npm version {major|minor|patch} @@ -71,17 +71,16 @@ See [deciding what type of version release section](#deciding-what-type-of-versi Optionally, you can run `git show` now to verify that the version update and CHANGELOG automation worked as expected. Afterwards, you want to push the commit and the tag to the repo. -It's necessary to do this before running `npm publish` because our GitHub release automation relies on the commit being available on GitHub. ```sh git push --follow-tags origin main ``` -Publish to npm. +After the tag was pushed, GitHub actions will trigger the `release` workflow, which will do the following: -```sh -npm publish -``` +* Publish to npm with `next` or `next-{n}` depending on your current major version. +* Create GitHub release with changelog and Netlify preview. +* Create a GitHub `releases` discussion linked to the GitHub release. If it's a large enough release, consider writing a blog post as well. diff --git a/package-lock.json b/package-lock.json index 4682c7c5c..dcd85fad9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@videojs/http-streaming", - "version": "3.0.1", + "version": "3.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -14,6 +14,47 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, + "@babel/cli": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.21.0.tgz", + "integrity": "sha512-xi7CxyS8XjSyiwUGCfwf+brtJxjW1/ZTcBUkP10xawIEXLX5HzLn+3aXkgxozcP2UhRhtKTmQurw9Uaes7jZrA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.17", + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.4.0", + "commander": "^4.0.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.2.0", + "make-dir": "^2.1.0", + "slash": "^2.0.0" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } + } + }, "@babel/code-frame": { "version": "7.15.8", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", @@ -1449,6 +1490,19 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", + "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "dev": true, + "optional": true + }, + "@popperjs/core": { + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", + "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==", + "dev": true + }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -1610,10 +1664,13 @@ "dev": true }, "@types/cors": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", - "dev": true + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "dev": true, + "requires": { + "@types/node": "*" + } }, "@types/estree": { "version": "0.0.39", @@ -1722,68 +1779,27 @@ } }, "@videojs/http-streaming": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.10.2.tgz", - "integrity": "sha512-JTAlAUHzj0sTsge2WBh4DWKM2I5BDFEZYOvzxmsK/ySILmI0GRyjAHx9uid68ZECQ2qbEAIRmZW5lWp0R5PeNA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.0.0.tgz", + "integrity": "sha512-AdKmY/W2dyeJP0uALgMRmhLa4pbHMvE4OMlg6yQvufnqsz6jDFo1DYnZRv2ENDYrmVdnPH58Ehgu59053+OIhQ==", "requires": { "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "3.0.3", - "aes-decrypter": "3.1.2", + "@videojs/vhs-utils": "4.0.0", + "aes-decrypter": "4.0.1", "global": "^4.4.0", - "m3u8-parser": "4.7.0", - "mpd-parser": "0.19.0", - "mux.js": "5.13.0", - "video.js": "^6 || ^7" + "m3u8-parser": "^6.0.0", + "mpd-parser": "^1.0.1", + "mux.js": "6.2.0", + "video.js": "^7 || ^8" }, "dependencies": { - "@videojs/vhs-utils": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.3.tgz", - "integrity": "sha512-bU7daxDHhzcTDbmty1cXjzsTYvx2cBGbA8hG5H2Gvpuk4sdfuvkZtMCwtCqL59p6dsleMPspyaNS+7tWXx2Y0A==", - "requires": { - "@babel/runtime": "^7.12.5", - "global": "^4.4.0", - "url-toolkit": "^2.2.1" - } - }, - "aes-decrypter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.2.tgz", - "integrity": "sha512-42nRwfQuPRj9R1zqZBdoxnaAmnIFyDi0MNyTVhjdFOd8fifXKKRfwIHIZ6AMn1or4x5WONzjwRTbTWcsIQ0O4A==", - "requires": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^3.0.0", - "global": "^4.4.0", - "pkcs7": "^1.0.4" - } - }, - "m3u8-parser": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.0.tgz", - "integrity": "sha512-48l/OwRyjBm+QhNNigEEcRcgbRvnUjL7rxs597HmW9QSNbyNvt+RcZ9T/d9vxi9A9z7EZrB1POtZYhdRlwYQkQ==", - "requires": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^3.0.0", - "global": "^4.4.0" - } - }, - "mpd-parser": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.19.0.tgz", - "integrity": "sha512-FDLIXtZMZs99fv5iXNFg94quNFT26tobo0NUgHu7L3XgZvEq1NBarf5yxDFFJ1zzfbcmtj+NRaml6nYIxoPWvw==", - "requires": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^3.0.2", - "@xmldom/xmldom": "^0.7.2", - "global": "^4.4.0" - } - }, "mux.js": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.13.0.tgz", - "integrity": "sha512-PkmnzHcTQjUBEHp3KRPQAFoNkJtKlpCEvsYtXDfDrC+/WqbMuxHvoYfmAbHVAH7Sa/KliPVU0dT1ureO8wilog==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.2.0.tgz", + "integrity": "sha512-SKuxIcbmK/aJoz78aQNuoXY8R/uEPm1gQMqWTXL6DNl7oF8UPjdt/AunXGkPQpBouGWKDgL/TzSl2VV5NuboRg==", "requires": { - "@babel/runtime": "^7.11.2" + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" } } } @@ -1824,9 +1840,10 @@ } }, "@xmldom/xmldom": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz", - "integrity": "sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==" + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.7.tgz", + "integrity": "sha512-sI1Ly2cODlWStkINzqGrZ8K6n+MTSbAeQnAipGyL+KZCXuHaRlj2gyyy8B/9MvsFFqN7XHryQnB2QwhzvJXovg==", + "dev": true }, "JSONStream": { "version": "1.3.5", @@ -2551,7 +2568,7 @@ "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "integrity": "sha512-wFUFA5bg5dviipbQQ32yOQhl6gcJaJXiHE7dvR8VYPG97+J/GNC5FKGepKdEDUFeXRzDxPF1X/Btc8L+v7oqIQ==", "dev": true }, "is-fullwidth-code-point": { @@ -2684,7 +2701,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "concat-stream": { @@ -3511,9 +3528,9 @@ "dev": true }, "engine.io": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", - "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", + "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", "dev": true, "requires": { "@types/cookie": "^0.4.1", @@ -3537,9 +3554,9 @@ } }, "engine.io-parser": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz", - "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", + "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==", "dev": true }, "enquirer": { @@ -4247,6 +4264,12 @@ "universalify": "^0.1.0" } }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5499,9 +5522,9 @@ "dev": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "jsonfile": { @@ -5892,7 +5915,7 @@ "keycode": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz", - "integrity": "sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ=" + "integrity": "sha512-ps3I9jAdNtRpJrbBvQjpzyFbss/skHqzS+eu4RxKLaEAtFqkjZaB6TZMSivPbLxf4K7VI4SjR0P5mRCX5+Q25A==" }, "kind-of": { "version": "6.0.3", @@ -6284,6 +6307,30 @@ "sourcemap-codec": "^1.4.4" } }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -6797,9 +6844,9 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -6835,9 +6882,9 @@ "dev": true }, "mpd-parser": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.0.1.tgz", - "integrity": "sha512-3CmVq7af3tcLqpx9SLBSweVnxS0SlpkKr+YBLC7O/wO1bv2L9tsCY350wziPWuMEjE5+x46i898sI4YIY3cmJA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.1.1.tgz", + "integrity": "sha512-uZ/db5wQdlQn1L+OD49YXBhPI9UGeK1SeQE4D5EoaJIhf0WM9X3HDj8d+9PjoG06CgCvGZw3YW/wsHku+CH3yA==", "requires": { "@babel/runtime": "^7.12.5", "@videojs/vhs-utils": "^3.0.5", @@ -6856,9 +6903,9 @@ } }, "@xmldom/xmldom": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.3.tgz", - "integrity": "sha512-Lv2vySXypg4nfa51LY1nU8yDAGo/5YwF+EY/rUZgIbfvwVARcd67ttCM8SMsTeJy51YhHYavEq+FS6R0hW9PFQ==" + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.6.tgz", + "integrity": "sha512-uRjjusqpoqfmRkTaNuLJ2VohVr67Q5YwDATW3VU7PfzTj6IRaihGrYI7zckGZjxQPBIp63nfvJbM+Yu5ICh0Bg==" } } }, @@ -6875,9 +6922,9 @@ "dev": true }, "mux.js": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.2.0.tgz", - "integrity": "sha512-SKuxIcbmK/aJoz78aQNuoXY8R/uEPm1gQMqWTXL6DNl7oF8UPjdt/AunXGkPQpBouGWKDgL/TzSl2VV5NuboRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.3.0.tgz", + "integrity": "sha512-/QTkbSAP2+w1nxV+qTcumSDN5PA98P0tjrADijIzQHe85oBK3Akhy9AHlH0ne/GombLMz1rLyvVsmrgRxoPDrQ==", "requires": { "@babel/runtime": "^7.11.2", "global": "^4.4.0" @@ -8980,9 +9027,9 @@ "dev": true }, "ua-parser-js": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", - "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==", + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.34.tgz", + "integrity": "sha512-cJMeh/eOILyGu0ejgTKB95yKT3zOenSe9UGE3vj6WfiOwgGYnmATUsnDixMFvdU+rNMvWih83hrUP8VwhF9yXQ==", "dev": true }, "uc.micro": { @@ -9176,74 +9223,33 @@ "dev": true }, "video.js": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.15.4.tgz", - "integrity": "sha512-hghxkgptLUvfkpktB4wxcIVF3VpY/hVsMkrjHSv0jpj1bW9Jplzdt8IgpTm9YhlB1KYAp07syVQeZcBFUBwhkw==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.0.4.tgz", + "integrity": "sha512-fvvWauPanrKDps1HQGGL+9CIAK8G0YVwlNme0hvY0k3moXQryaRcJSLHIlPKV2j9ZFTHl32VbN43jL3TaUllfg==", "requires": { "@babel/runtime": "^7.12.5", - "@videojs/http-streaming": "2.10.2", - "@videojs/vhs-utils": "^3.0.3", + "@videojs/http-streaming": "3.0.0", + "@videojs/vhs-utils": "^4.0.0", "@videojs/xhr": "2.6.0", - "aes-decrypter": "3.1.2", - "global": "^4.4.0", - "keycode": "^2.2.0", - "m3u8-parser": "4.7.0", - "mpd-parser": "0.19.0", - "mux.js": "5.13.0", + "aes-decrypter": "^4.0.1", + "global": "4.4.0", + "keycode": "2.2.0", + "m3u8-parser": "^6.0.0", + "mpd-parser": "^1.0.1", + "mux.js": "^6.2.0", "safe-json-parse": "4.0.0", + "videojs-contrib-quality-levels": "3.0.0", "videojs-font": "3.2.0", - "videojs-vtt.js": "^0.15.3" + "videojs-vtt.js": "0.15.4" }, "dependencies": { - "@videojs/vhs-utils": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", - "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", - "requires": { - "@babel/runtime": "^7.12.5", - "global": "^4.4.0", - "url-toolkit": "^2.2.1" - } - }, - "aes-decrypter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.2.tgz", - "integrity": "sha512-42nRwfQuPRj9R1zqZBdoxnaAmnIFyDi0MNyTVhjdFOd8fifXKKRfwIHIZ6AMn1or4x5WONzjwRTbTWcsIQ0O4A==", - "requires": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^3.0.0", - "global": "^4.4.0", - "pkcs7": "^1.0.4" - } - }, - "m3u8-parser": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.0.tgz", - "integrity": "sha512-48l/OwRyjBm+QhNNigEEcRcgbRvnUjL7rxs597HmW9QSNbyNvt+RcZ9T/d9vxi9A9z7EZrB1POtZYhdRlwYQkQ==", - "requires": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^3.0.0", - "global": "^4.4.0" - } - }, - "mpd-parser": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.19.0.tgz", - "integrity": "sha512-FDLIXtZMZs99fv5iXNFg94quNFT26tobo0NUgHu7L3XgZvEq1NBarf5yxDFFJ1zzfbcmtj+NRaml6nYIxoPWvw==", + "videojs-contrib-quality-levels": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-3.0.0.tgz", + "integrity": "sha512-sNx38EYUx+Q+gmup1gVTv9P9/sPs28rM7gZOx1sedaHoKxEdYB+ysOGfHj6MSELBMNGMj6ZspdrpSiWguGvGxA==", "requires": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^3.0.2", - "@xmldom/xmldom": "^0.7.2", "global": "^4.4.0" } - }, - "mux.js": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.13.0.tgz", - "integrity": "sha512-PkmnzHcTQjUBEHp3KRPQAFoNkJtKlpCEvsYtXDfDrC+/WqbMuxHvoYfmAbHVAH7Sa/KliPVU0dT1ureO8wilog==", - "requires": { - "@babel/runtime": "^7.11.2" - } } } }, @@ -9258,13 +9264,12 @@ } }, "videojs-contrib-quality-levels": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-2.1.0.tgz", - "integrity": "sha512-dqGQGbL9AFhucxki7Zh0c3kIhH0PAPcHEh6jUdRyaFCVeOuqnJrOYs/3wNtsokDdBdRf2Du2annpu4Z2XaSZRg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.0.0.tgz", + "integrity": "sha512-u5rmd8BjLwANp7XwuQ0Q/me34bMe6zg9PQdHfTS7aXgiVRbNTb4djcmfG7aeSrkpZjg+XCLezFNenlJaCjBHKw==", "dev": true, "requires": { - "global": "^4.3.2", - "video.js": "^6 || ^7" + "global": "^4.4.0" } }, "videojs-font": { @@ -9347,6 +9352,111 @@ "global": "^4.3.2", "video.js": "^7.0.0", "videojs-contrib-quality-levels": "^2.0.4" + }, + "dependencies": { + "@videojs/http-streaming": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.16.2.tgz", + "integrity": "sha512-etPTUdCFu7gUWc+1XcbiPr+lrhOcBu3rV5OL1M+3PDW89zskScAkkcdqYzP4pFodBPye/ydamQoTDScOnElw5A==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "3.0.5", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "^0.22.1", + "mux.js": "6.0.1", + "video.js": "^6 || ^7" + } + }, + "@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + } + }, + "aes-decrypter": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz", + "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "m3u8-parser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.8.0.tgz", + "integrity": "sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0" + } + }, + "mpd-parser": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.22.1.tgz", + "integrity": "sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + } + }, + "mux.js": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz", + "integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==", + "dev": true, + "requires": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + } + }, + "video.js": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.21.4.tgz", + "integrity": "sha512-R5e57M/5uqxQMQpFpybNbd8GtiRwFJPqkHjrhv0QTJ2tqnesbjETbck5kU5dhFr1FevsJRFhjBG4hAnvRGnXbw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "2.16.2", + "@videojs/vhs-utils": "^3.0.4", + "@videojs/xhr": "2.6.0", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "keycode": "^2.2.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "0.22.1", + "mux.js": "6.0.1", + "safe-json-parse": "4.0.0", + "videojs-font": "3.2.0", + "videojs-vtt.js": "^0.15.4" + } + }, + "videojs-contrib-quality-levels": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-2.2.1.tgz", + "integrity": "sha512-cnF6OGGgoC/2nUrbdz54nzPm3BpEZQzMTpyekiX6AXs8imATX2sHbrUz97xXVSHITldk/+d7ZAUrdQYJJTyuug==", + "dev": true, + "requires": { + "global": "^4.3.2", + "video.js": "^6 || ^7 || ^8" + } + } } }, "videojs-standard": { @@ -9374,9 +9484,9 @@ } }, "videojs-vtt.js": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.3.tgz", - "integrity": "sha512-5FvVsICuMRx6Hd7H/Y9s9GDeEtYcXQWzGMS+sl4UX3t/zoHp3y+isSfIPRochnTH7h+Bh1ILyC639xy9Z6kPag==", + "version": "0.15.4", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.4.tgz", + "integrity": "sha512-r6IhM325fcLb1D6pgsMkTQT1PpFdUdYZa1iqk7wJEu+QlibBwATPfPc9Bg8Jiym0GE5yP1AG2rMLu+QMVWkYtA==", "requires": { "global": "^4.3.1" } diff --git a/package.json b/package.json index afdd37a67..e3becbca8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@videojs/http-streaming", - "version": "3.0.1", + "version": "3.1.0", "description": "Play back HLS and DASH with Video.js, even where it's not natively supported", "main": "dist/videojs-http-streaming.cjs.js", "module": "dist/videojs-http-streaming.es.js", @@ -62,14 +62,16 @@ "aes-decrypter": "4.0.1", "global": "^4.4.0", "m3u8-parser": "^6.0.0", - "mpd-parser": "^1.0.1", - "mux.js": "6.2.0", + "mpd-parser": "^1.1.1", + "mux.js": "6.3.0", "video.js": "^7 || ^8" }, "peerDependencies": { "video.js": "^7 || ^8" }, "devDependencies": { + "@babel/cli": "^7.21.0", + "@popperjs/core": "^2.11.7", "@rollup/plugin-replace": "^2.3.4", "@rollup/plugin-strip": "^2.0.1", "@videojs/generator-helpers": "~3.1.0", @@ -86,7 +88,7 @@ "sinon": "^8.1.1", "url-toolkit": "^2.2.1", "videojs-contrib-eme": "^5.0.1", - "videojs-contrib-quality-levels": "^2.0.4", + "videojs-contrib-quality-levels": "^4.0.0", "videojs-generate-karma-config": "^8.0.1", "videojs-generate-rollup-config": "^7.0.0", "videojs-generator-verify": "~3.0.1", diff --git a/src/media-segment-request.js b/src/media-segment-request.js index 0a88b63ce..086f0fe48 100644 --- a/src/media-segment-request.js +++ b/src/media-segment-request.js @@ -462,7 +462,7 @@ const handleSegmentBytes = ({ // Note that the start time returned by the probe reflects the baseMediaDecodeTime, as // that is the true start of the segment (where the playback engine should begin // decoding). - const finishLoading = (captions) => { + const finishLoading = (captions, id3Frames) => { // if the track still has audio at this point it is only possible // for it to be audio only. See `tracks.video && tracks.audio` if statement // above. @@ -471,6 +471,9 @@ const handleSegmentBytes = ({ data: bytesAsUint8Array, type: trackInfo.hasAudio && !trackInfo.isMuxed ? 'audio' : 'video' }); + if (id3Frames && id3Frames.length) { + id3Fn(segment, id3Frames); + } if (captions && captions.length) { captionsFn(segment, captions); } @@ -494,29 +497,40 @@ const handleSegmentBytes = ({ if (trackInfo.hasVideo) { timingInfoFn(segment, 'video', 'start', startTime); } - - // Run through the CaptionParser in case there are captions. - // Initialize CaptionParser if it hasn't been yet - if (!tracks.video || !data.byteLength || !segment.transmuxer) { - finishLoading(); - return; - } - workerCallback({ - action: 'pushMp4Captions', - endAction: 'mp4Captions', - transmuxer: segment.transmuxer, + action: 'probeEmsgID3', data: bytesAsUint8Array, - timescales: segment.map.timescales, - trackIds: [tracks.video.id], - callback: (message) => { + transmuxer: segment.transmuxer, + offset: startTime, + callback: ({emsgData, id3Frames}) => { // transfer bytes back to us - bytes = message.data.buffer; - segment.bytes = bytesAsUint8Array = message.data; - message.logs.forEach(function(log) { - onTransmuxerLog(merge(log, {stream: 'mp4CaptionParser'})); + bytes = emsgData.buffer; + segment.bytes = bytesAsUint8Array = emsgData; + + // Run through the CaptionParser in case there are captions. + // Initialize CaptionParser if it hasn't been yet + if (!tracks.video || !data.byteLength || !segment.transmuxer) { + finishLoading(undefined, id3Frames); + return; + } + + workerCallback({ + action: 'pushMp4Captions', + endAction: 'mp4Captions', + transmuxer: segment.transmuxer, + data: bytesAsUint8Array, + timescales: segment.map.timescales, + trackIds: [tracks.video.id], + callback: (message) => { + // transfer bytes back to us + bytes = message.data.buffer; + segment.bytes = bytesAsUint8Array = message.data; + message.logs.forEach(function(log) { + onTransmuxerLog(merge(log, {stream: 'mp4CaptionParser'})); + }); + finishLoading(message.captions, id3Frames); + } }); - finishLoading(message.captions); } }); } diff --git a/src/playlist-controller.js b/src/playlist-controller.js index aed3786a7..79f0da771 100644 --- a/src/playlist-controller.js +++ b/src/playlist-controller.js @@ -933,18 +933,12 @@ export class PlaylistController extends videojs.EventTarget { // Delete all buffered data to allow an immediate quality switch, then seek to give // the browser a kick to remove any cached frames from the previous rendtion (.04 seconds - // ahead is roughly the minimum that will accomplish this across a variety of content + // ahead was roughly the minimum that will accomplish this across a variety of content // in IE and Edge, but seeking in place is sufficient on all other browsers) // Edge/IE bug: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14600375/ // Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=651904 this.mainSegmentLoader_.resetEverything(() => { - // Since this is not a typical seek, we avoid the seekTo method which can cause segments - // from the previously enabled rendition to load before the new playlist has finished loading - if (videojs.browser.IE_VERSION || videojs.browser.IS_EDGE) { - this.tech_.setCurrentTime(this.tech_.currentTime() + 0.04); - } else { - this.tech_.setCurrentTime(this.tech_.currentTime()); - } + this.tech_.setCurrentTime(this.tech_.currentTime()); }); // don't need to reset audio as it is reset when media changes @@ -1003,19 +997,6 @@ export class PlaylistController extends videojs.EventTarget { return false; } - if (videojs.browser.IE_VERSION && - this.tech_.readyState() === 0) { - // IE11 throws an InvalidStateError if you try to set currentTime while the - // readyState is 0, so it must be delayed until the tech fires loadedmetadata. - this.tech_.one('loadedmetadata', () => { - this.trigger('firstplay'); - this.tech_.setCurrentTime(seekable.end(0)); - this.hasPlayed_ = true; - }); - - return false; - } - // trigger firstplay to inform the source handler to ignore the next seek event this.trigger('firstplay'); // seek to the live point @@ -1715,9 +1696,11 @@ export class PlaylistController extends videojs.EventTarget { audio: this.audioSegmentLoader_.getCurrentMediaInfo_() || {} }; + const playlist = this.mainSegmentLoader_.getPendingSegmentPlaylist() || this.media(); + // set "main" media equal to video media.video = media.main; - const playlistCodecs = codecsForPlaylist(this.main(), this.media()); + const playlistCodecs = codecsForPlaylist(this.main(), playlist); const codecs = {}; const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader; @@ -1738,7 +1721,7 @@ export class PlaylistController extends videojs.EventTarget { // no codecs, no playback. if (!codecs.audio && !codecs.video) { this.excludePlaylist({ - playlistToExclude: this.media(), + playlistToExclude: playlist, error: { message: 'Could not determine codecs for playlist.' }, playlistExclusionDuration: Infinity }); @@ -1763,13 +1746,13 @@ export class PlaylistController extends videojs.EventTarget { } }); - if (usingAudioLoader && unsupportedAudio && this.media().attributes.AUDIO) { - const audioGroup = this.media().attributes.AUDIO; + if (usingAudioLoader && unsupportedAudio && playlist.attributes.AUDIO) { + const audioGroup = playlist.attributes.AUDIO; this.main().playlists.forEach(variant => { const variantAudioGroup = variant.attributes && variant.attributes.AUDIO; - if (variantAudioGroup === audioGroup && variant !== this.media()) { + if (variantAudioGroup === audioGroup && variant !== playlist) { variant.excludeUntil = Infinity; } }); @@ -1790,7 +1773,7 @@ export class PlaylistController extends videojs.EventTarget { }, '') + '.'; this.excludePlaylist({ - playlistToExclude: this.media(), + playlistToExclude: playlist, error: { internal: true, message @@ -1817,7 +1800,7 @@ export class PlaylistController extends videojs.EventTarget { if (switchMessages.length) { this.excludePlaylist({ - playlistToExclude: this.media(), + playlistToExclude: playlist, error: { message: `Codec switching not supported: ${switchMessages.join(', ')}.`, internal: true diff --git a/src/segment-loader.js b/src/segment-loader.js index 3b26a2f04..5428a8347 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -576,7 +576,7 @@ export default class SegmentLoader extends videojs.EventTarget { // TODO possibly move gopBuffer and timeMapping info to a separate controller this.gopBuffer_ = []; this.timeMapping_ = 0; - this.safeAppend_ = videojs.browser.IE_VERSION >= 11; + this.safeAppend_ = false; this.appendInitSegment_ = { audio: true, video: true @@ -1163,6 +1163,7 @@ export default class SegmentLoader extends videojs.EventTarget { */ resetEverything(done) { this.ended_ = false; + this.activeInitSegmentId_ = null; this.appendInitSegment_ = { audio: true, video: true @@ -1983,6 +1984,10 @@ export default class SegmentLoader extends videojs.EventTarget { return this.getCurrentMediaInfo_(segmentInfo) || this.startingMediaInfo_; } + getPendingSegmentPlaylist() { + return this.pendingSegment_ ? this.pendingSegment_.playlist : null; + } + hasEnoughInfoToAppend_() { if (!this.sourceUpdater_.ready()) { return false; diff --git a/src/source-updater.js b/src/source-updater.js index 5b4f541b7..a7f3f11da 100644 --- a/src/source-updater.js +++ b/src/source-updater.js @@ -468,11 +468,9 @@ export default class SourceUpdater extends videojs.EventTarget { * if removeSourceBuffer can be called. */ canRemoveSourceBuffer() { - // IE reports that it supports removeSourceBuffer, but often throws - // errors when attempting to use the function. So we report that it - // does not support removeSourceBuffer. As of Firefox 83 removeSourceBuffer - // throws errors, so we report that it does not support this as well. - return !videojs.browser.IE_VERSION && !videojs.browser.IS_FIREFOX && window.MediaSource && + // As of Firefox 83 removeSourceBuffer + // throws errors, so we report that it does not support this. + return !videojs.browser.IS_FIREFOX && window.MediaSource && window.MediaSource.prototype && typeof window.MediaSource.prototype.removeSourceBuffer === 'function'; } diff --git a/src/transmuxer-worker.js b/src/transmuxer-worker.js index d8f9e7e3f..049e221ba 100644 --- a/src/transmuxer-worker.js +++ b/src/transmuxer-worker.js @@ -227,6 +227,24 @@ class MessageHandlers { }, [data.buffer]); } + /** + * Probes an mp4 segment for EMSG boxes containing ID3 data. + * https://aomediacodec.github.io/id3-emsg/ + * + * @param {Uint8Array} data segment data + * @param {number} offset segment start time + * @return {Object[]} an array of ID3 frames + */ + probeEmsgID3({data, offset}) { + const id3Frames = mp4probe.getEmsgID3(data, offset); + + this.self.postMessage({ + action: 'probeEmsgID3', + id3Frames, + emsgData: data + }, [data.buffer]); + } + /** * Probe an mpeg2-ts segment to determine the start time of the segment in it's * internal "media time," as well as whether it contains video and/or audio. diff --git a/src/util/text-tracks.js b/src/util/text-tracks.js index 1f9562b25..699c26d1c 100644 --- a/src/util/text-tracks.js +++ b/src/util/text-tracks.js @@ -159,6 +159,11 @@ export const addMetadata = ({ return; } + // If we have no frames, we can't create a cue. + if (!metadata.frames || !metadata.frames.length) { + return; + } + metadata.frames.forEach((frame) => { const cue = new Cue( time, @@ -285,26 +290,16 @@ export const removeDuplicateCuesFromTrack = function(track) { return; } - for (let i = 0; i < cues.length; i++) { - const duplicates = []; - let occurrences = 0; - - for (let j = 0; j < cues.length; j++) { - if ( - cues[i].startTime === cues[j].startTime && - cues[i].endTime === cues[j].endTime && - cues[i].text === cues[j].text - ) { - occurrences++; - - if (occurrences > 1) { - duplicates.push(cues[j]); - } - } - } + const uniqueCues = {}; + + for (let i = cues.length - 1; i >= 0; i--) { + const cue = cues[i]; + const cueKey = `${cue.startTime}-${cue.endTime}-${cue.text}`; - if (duplicates.length) { - duplicates.forEach(dupe => track.removeCue(dupe)); + if (uniqueCues[cueKey]) { + track.removeCue(cue); + } else { + uniqueCues[cueKey] = cue; } } }; diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index 1869a9b0b..e7833d285 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -287,8 +287,8 @@ export const waitForKeySessionCreation = ({ const keySessionCreatedPromises = []; // Since PSSH values are interpreted as initData, EME will dedupe any duplicates. The - // only place where it should not be deduped is for ms-prefixed APIs, but the early - // return for IE11 above, and the existence of modern EME APIs in addition to + // only place where it should not be deduped is for ms-prefixed APIs, but + // the existence of modern EME APIs in addition to // ms-prefixed APIs on Edge should prevent this from being a concern. // initializeMediaKeys also won't use the webkit-prefixed APIs. keySystemsOptionsArr.forEach((keySystemsOptions) => { @@ -1060,9 +1060,7 @@ class VhsHandler extends Component { this.handleWaitingForKey_ = this.handleWaitingForKey_.bind(this); this.player_.tech_.on('waitingforkey', this.handleWaitingForKey_); - // In IE11 this is too early to initialize media keys, and IE11 does not support - // promises. - if (videojs.browser.IE_VERSION === 11 || !didSetupEmeOptions) { + if (!didSetupEmeOptions) { // If EME options were not set up, we've done all we could to initialize EME. this.playlistController_.sourceUpdater_.initializedEme(); return; diff --git a/test/loader-common.js b/test/loader-common.js index 6681c6094..0016b114d 100644 --- a/test/loader-common.js +++ b/test/loader-common.js @@ -1,5 +1,4 @@ import QUnit from 'qunit'; -import videojs from 'video.js'; import xhrFactory from '../src/xhr'; import Config from '../src/config'; import document from 'global/document'; @@ -941,16 +940,7 @@ export const LoaderCommonFactory = ({ // only main/fmp4 segment loaders use async appends and parts/partIndex if (usesAsyncAppends) { - let testFn = 'test'; - - if (videojs.browser.IE_VERSION) { - testFn = 'skip'; - } - - // this test has a race condition on ie 11 that causes it to fail some of the time. - // Since IE 11 isn't really a priority and it only fails some of the time we decided to - // skip this on IE 11. - QUnit[testFn]('playlist change before any appends does not error', function(assert) { + QUnit.test('playlist change before any appends does not error', function(assert) { return this.setupMediaSource(loader.mediaSource_, loader.sourceUpdater_).then(() => { loader.playlist(playlistWithDuration(50, { uri: 'bar-720.m3u8', diff --git a/test/media-segment-request.test.js b/test/media-segment-request.test.js index 4ca5c1a81..bfb89843e 100644 --- a/test/media-segment-request.test.js +++ b/test/media-segment-request.test.js @@ -15,6 +15,8 @@ import { ac3WithoutId3 as ac3WithoutId3Segment, video as videoSegment, audio as audioSegment, + mp4Audio, + mp4AudioInit, mp4Video, mp4VideoInit, muxed as muxedSegment, @@ -1357,6 +1359,17 @@ QUnit.test('non-TS segment will get parsed for captions', function(assert) { } }); } + + if (event.action === 'probeEmsgID3') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'probeEmsgID3', + emsgData: event.data, + id3Frames: [] + } + }); + } }; mediaSegmentRequest({ @@ -1498,6 +1511,17 @@ QUnit.test('non-TS segment will get parsed for captions on next segment request } }); } + + if (event.action === 'probeEmsgID3') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'probeEmsgID3', + emsgData: event.data, + id3Frames: [] + } + }); + } }; mediaSegmentRequest({ @@ -1553,3 +1577,263 @@ QUnit.test('non-TS segment will get parsed for captions on next segment request // Simulate receiving the init segment after the media this.standardXHRResponse(initReq, mp4VideoInit()); }); + +QUnit.test('can get emsg ID3 frames from fmp4 video segment', function(assert) { + const done = assert.async(); + let gotEmsgId3 = 0; + let gotData = 0; + // expected frame data + const id3Frames = [{ + cueTime: 1, + duration: 0, + frames: [{ + id: 'TXXX', + description: 'foo bar', + data: { key: 'value' } + }, + { + id: 'PRIV', + owner: 'priv-owner@foo.bar', + // 'foo' + data: new Uint8Array([0x66, 0x6F, 0x6F]) + }] + }, + { + cueTime: 3, + duration: 0, + frames: [{ + id: 'PRIV', + owner: 'priv-owner@foo.bar', + // 'bar' + data: new Uint8Array([0x62, 0x61, 0x72]) + }, + { + id: 'TXXX', + description: 'bar foo', + data: { key: 'value' } + }] + }]; + const transmuxer = new videojs.EventTarget(); + + transmuxer.postMessage = (event) => { + if (event.action === 'pushMp4Captions') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'mp4Captions', + data: event.data, + captions: 'foo bar', + logs: [] + } + }); + } + + if (event.action === 'probeMp4StartTime') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'probeMp4StartTime', + data: event.data, + timingInfo: {} + } + }); + } + + if (event.action === 'probeMp4Tracks') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'probeMp4Tracks', + data: event.data, + tracks: [{type: 'video', codec: 'avc1.4d400d'}] + } + }); + } + + if (event.action === 'probeEmsgID3') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'probeEmsgID3', + emsgData: event.data, + id3Frames + } + }); + } + }; + + mediaSegmentRequest({ + xhr: this.xhr, + xhrOptions: this.xhrOptions, + decryptionWorker: this.mockDecrypter, + segment: { + transmuxer, + resolvedUri: 'mp4Video.mp4', + map: { + resolvedUri: 'mp4VideoInit.mp4' + } + }, + progressFn: this.noop, + trackInfoFn: this.noop, + timingInfoFn: this.noop, + id3Fn: (segment, _id3Frames) => { + gotEmsgId3++; + assert.deepEqual(_id3Frames, id3Frames, 'got expected emsg id3 data.'); + }, + captionsFn: this.noop, + dataFn: (segment, segmentData) => { + gotData++; + assert.ok(segmentData, 'init segment bytes in map'); + assert.ok(segment.map.tracks, 'added tracks'); + assert.ok(segment.map.tracks.video, 'added video track'); + }, + doneFn: () => { + assert.equal(gotEmsgId3, 1, 'received emsg ID3 event'); + assert.equal(gotData, 1, 'received data event'); + transmuxer.off(); + done(); + } + }); + assert.equal(this.requests.length, 2, 'there are two requests'); + + const initReq = this.requests.shift(); + const segmentReq = this.requests.shift(); + + assert.equal(initReq.uri, 'mp4VideoInit.mp4', 'the first request is for the init segment'); + assert.equal(segmentReq.uri, 'mp4Video.mp4', 'the second request is for a segment'); + + // Simulate receiving the media first + this.standardXHRResponse(segmentReq, mp4Video()); + // Simulate receiving the init segment after the media + this.standardXHRResponse(initReq, mp4VideoInit()); +}); + +QUnit.test('can get emsg ID3 frames from fmp4 audio segment', function(assert) { + const done = assert.async(); + let gotEmsgId3 = 0; + let gotData = 0; + // expected frame data + const id3Frames = [{ + cueTime: 1, + duration: 0, + frames: [{ + id: 'TXXX', + description: 'foo bar', + data: { key: 'value' } + }, + { + id: 'PRIV', + owner: 'priv-owner@foo.bar', + // 'foo' + data: new Uint8Array([0x66, 0x6F, 0x6F]) + }] + }, + { + cueTime: 3, + duration: 0, + frames: [{ + id: 'PRIV', + owner: 'priv-owner@foo.bar', + // 'bar' + data: new Uint8Array([0x62, 0x61, 0x72]) + }, + { + id: 'TXXX', + description: 'bar foo', + data: { key: 'value' } + }] + }]; + const transmuxer = new videojs.EventTarget(); + + transmuxer.postMessage = (event) => { + if (event.action === 'pushMp4Captions') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'mp4Captions', + data: event.data, + captions: 'foo bar', + logs: [] + } + }); + } + + if (event.action === 'probeMp4StartTime') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'probeMp4StartTime', + data: event.data, + timingInfo: {} + } + }); + } + + if (event.action === 'probeMp4Tracks') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'probeMp4Tracks', + data: event.data, + tracks: [{type: 'audio', codec: 'mp4a.40.2'}] + } + }); + } + + if (event.action === 'probeEmsgID3') { + transmuxer.trigger({ + type: 'message', + data: { + action: 'probeEmsgID3', + emsgData: event.data, + id3Frames + } + }); + } + }; + + mediaSegmentRequest({ + xhr: this.xhr, + xhrOptions: this.xhrOptions, + decryptionWorker: this.mockDecrypter, + segment: { + transmuxer, + resolvedUri: 'mp4Audio.mp4', + map: { + resolvedUri: 'mp4AudioInit.mp4' + } + }, + progressFn: this.noop, + trackInfoFn: this.noop, + timingInfoFn: this.noop, + id3Fn: (segment, _id3Frames) => { + gotEmsgId3++; + assert.deepEqual(_id3Frames, id3Frames, 'got expected emsg id3 data.'); + }, + captionsFn: this.noop, + dataFn: (segment, segmentData) => { + gotData++; + assert.ok(segmentData, 'init segment bytes in map'); + assert.ok(segment.map.tracks, 'added tracks'); + assert.ok(segment.map.tracks.audio, 'added audio track'); + }, + doneFn: () => { + assert.equal(gotEmsgId3, 1, 'received emsg ID3 event'); + assert.equal(gotData, 1, 'received data event'); + transmuxer.off(); + done(); + } + }); + assert.equal(this.requests.length, 2, 'there are two requests'); + + const initReq = this.requests.shift(); + const segmentReq = this.requests.shift(); + + assert.equal(initReq.uri, 'mp4AudioInit.mp4', 'the first request is for the init segment'); + assert.equal(segmentReq.uri, 'mp4Audio.mp4', 'the second request is for a segment'); + + // Simulate receiving the media first + this.standardXHRResponse(segmentReq, mp4Audio()); + // Simulate receiving the init segment after the media + this.standardXHRResponse(initReq, mp4AudioInit()); +}); diff --git a/test/playback.test.js b/test/playback.test.js index 25d658278..07a7215eb 100644 --- a/test/playback.test.js +++ b/test/playback.test.js @@ -34,13 +34,6 @@ const playFor = function(player, time, cb) { checkPlayerTime(); }; -let testFn = 'test'; - -// TODO: get these tests working, right now we just one the one basic test -if (videojs.browser.IE_VERSION || videojs.browser.IS_EDGE) { - testFn = 'skip'; -} - QUnit.module('Playback', { beforeEach(assert) { assert.timeout(50000); @@ -117,7 +110,7 @@ QUnit.test('Advanced Bip Bop', function(assert) { }); }); -QUnit[testFn]('replay', function(assert) { +QUnit.test('replay', function(assert) { const done = assert.async(); assert.expect(2); @@ -145,7 +138,7 @@ QUnit[testFn]('replay', function(assert) { }); }); -QUnit[testFn]('playlist with fmp4 segments', function(assert) { +QUnit.test('playlist with fmp4 segments', function(assert) { const done = assert.async(); assert.expect(2); @@ -164,7 +157,7 @@ QUnit[testFn]('playlist with fmp4 segments', function(assert) { }); }); -QUnit[testFn]('playlist with fmp4 and ts segments', function(assert) { +QUnit.test('playlist with fmp4 and ts segments', function(assert) { const done = assert.async(); assert.expect(2); @@ -183,7 +176,7 @@ QUnit[testFn]('playlist with fmp4 and ts segments', function(assert) { }); }); -QUnit[testFn]('Advanced Bip Bop preload=none', function(assert) { +QUnit.test('Advanced Bip Bop preload=none', function(assert) { const done = assert.async(); assert.expect(2); @@ -204,7 +197,7 @@ QUnit[testFn]('Advanced Bip Bop preload=none', function(assert) { }); }); -QUnit[testFn]('Big Buck Bunny', function(assert) { +QUnit.test('Big Buck Bunny', function(assert) { const done = assert.async(); assert.expect(2); @@ -223,7 +216,7 @@ QUnit[testFn]('Big Buck Bunny', function(assert) { }); }); -QUnit[testFn]('Live DASH', function(assert) { +QUnit.test('Live DASH', function(assert) { const done = assert.async(); const player = this.player; @@ -257,7 +250,7 @@ QUnit[testFn]('Live DASH', function(assert) { player.play(); }); -QUnit[testFn]('Multiperiod dash works and can end', function(assert) { +QUnit.test('Multiperiod dash works and can end', function(assert) { const done = assert.async(); assert.expect(2); @@ -287,7 +280,7 @@ QUnit[testFn]('Multiperiod dash works and can end', function(assert) { // firefox has lower performance or more aggressive throttling than chrome // which causes a variety of issues. if (!videojs.browser.IS_FIREFOX) { - QUnit[testFn]('Big Buck Bunny audio only, groups & renditions same uri', function(assert) { + QUnit.test('Big Buck Bunny audio only, groups & renditions same uri', function(assert) { const done = assert.async(); assert.expect(2); @@ -306,7 +299,7 @@ if (!videojs.browser.IS_FIREFOX) { }); }); - QUnit[testFn]('Big Buck Bunny Demuxed av, audio only rendition same as group', function(assert) { + QUnit.test('Big Buck Bunny Demuxed av, audio only rendition same as group', function(assert) { const done = assert.async(); assert.expect(2); @@ -325,7 +318,7 @@ if (!videojs.browser.IS_FIREFOX) { }); }); - QUnit[testFn]('DASH sidx', function(assert) { + QUnit.test('DASH sidx', function(assert) { const done = assert.async(); const player = this.player; @@ -349,7 +342,7 @@ if (!videojs.browser.IS_FIREFOX) { }); }); - QUnit[testFn]('DASH sidx with alt audio should end', function(assert) { + QUnit.test('DASH sidx with alt audio should end', function(assert) { const done = assert.async(); const player = this.player; @@ -376,7 +369,7 @@ if (!videojs.browser.IS_FIREFOX) { }); }); - QUnit[testFn]('DRM Dash', function(assert) { + QUnit.test('DRM Dash', function(assert) { const done = assert.async(); const player = this.player; @@ -419,7 +412,7 @@ if (!videojs.browser.IS_FIREFOX) { // TODO: why does this make the next test // throw an "The operation was aborted." on firefox - QUnit[testFn]('loops', function(assert) { + QUnit.test('loops', function(assert) { const done = assert.async(); const player = this.player; @@ -444,7 +437,7 @@ if (!videojs.browser.IS_FIREFOX) { }); } -QUnit[testFn]('zero-length id3 segment', function(assert) { +QUnit.test('zero-length id3 segment', function(assert) { const done = assert.async(); const player = this.player; @@ -465,7 +458,7 @@ QUnit[testFn]('zero-length id3 segment', function(assert) { const hlsDataUri = 'data:application/x-mpegurl;charset=utf-8,%23EXTM3U%0D%0A%0D%0A%23EXT-X-MEDIA%3ATYPE%3DAUDIO%2CGROUP-ID%3D%22bipbop_audio%22%2CLANGUAGE%3D%22eng%22%2CNAME%3D%22BipBop%20Audio%201%22%2CAUTOSELECT%3DYES%2CDEFAULT%3DYES%0D%0A%23EXT-X-MEDIA%3ATYPE%3DAUDIO%2CGROUP-ID%3D%22bipbop_audio%22%2CLANGUAGE%3D%22eng%22%2CNAME%3D%22BipBop%20Audio%202%22%2CAUTOSELECT%3DNO%2CDEFAULT%3DNO%2CURI%3D%22https%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Falternate_audio_aac_sinewave%2Fprog_index.m3u8%22%0D%0A%0D%0A%0D%0A%23EXT-X-MEDIA%3ATYPE%3DSUBTITLES%2CGROUP-ID%3D%22subs%22%2CNAME%3D%22English%22%2CDEFAULT%3DYES%2CAUTOSELECT%3DYES%2CFORCED%3DNO%2CLANGUAGE%3D%22en%22%2CCHARACTERISTICS%3D%22public.accessibility.transcribes-spoken-dialog%2C%20public.accessibility.describes-music-and-sound%22%2CURI%3D%22https%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fsubtitles%2Feng%2Fprog_index.m3u8%22%0D%0A%23EXT-X-MEDIA%3ATYPE%3DSUBTITLES%2CGROUP-ID%3D%22subs%22%2CNAME%3D%22English%20%28Forced%29%22%2CDEFAULT%3DNO%2CAUTOSELECT%3DNO%2CFORCED%3DYES%2CLANGUAGE%3D%22en%22%2CURI%3D%22https%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fsubtitles%2Feng_forced%2Fprog_index.m3u8%22%0D%0A%23EXT-X-MEDIA%3ATYPE%3DSUBTITLES%2CGROUP-ID%3D%22subs%22%2CNAME%3D%22Fran%C3%83%C2%A7ais%22%2CDEFAULT%3DNO%2CAUTOSELECT%3DYES%2CFORCED%3DNO%2CLANGUAGE%3D%22fr%22%2CCHARACTERISTICS%3D%22public.accessibility.transcribes-spoken-dialog%2C%20public.accessibility.describes-music-and-sound%22%2CURI%3D%22https%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fsubtitles%2Ffra%2Fprog_index.m3u8%22%0D%0A%23EXT-X-MEDIA%3ATYPE%3DSUBTITLES%2CGROUP-ID%3D%22subs%22%2CNAME%3D%22Fran%C3%83%C2%A7ais%20%28Forced%29%22%2CDEFAULT%3DNO%2CAUTOSELECT%3DNO%2CFORCED%3DYES%2CLANGUAGE%3D%22fr%22%2CURI%3D%22https%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fsubtitles%2Ffra_forced%2Fprog_index.m3u8%22%0D%0A%23EXT-X-MEDIA%3ATYPE%3DSUBTITLES%2CGROUP-ID%3D%22subs%22%2CNAME%3D%22Espa%C3%83%C2%B1ol%22%2CDEFAULT%3DNO%2CAUTOSELECT%3DYES%2CFORCED%3DNO%2CLANGUAGE%3D%22es%22%2CCHARACTERISTICS%3D%22public.accessibility.transcribes-spoken-dialog%2C%20public.accessibility.describes-music-and-sound%22%2CURI%3D%22https%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fsubtitles%2Fspa%2Fprog_index.m3u8%22%0D%0A%23EXT-X-MEDIA%3ATYPE%3DSUBTITLES%2CGROUP-ID%3D%22subs%22%2CNAME%3D%22Espa%C3%83%C2%B1ol%20%28Forced%29%22%2CDEFAULT%3DNO%2CAUTOSELECT%3DNO%2CFORCED%3DYES%2CLANGUAGE%3D%22es%22%2CURI%3D%22https%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fsubtitles%2Fspa_forced%2Fprog_index.m3u8%22%0D%0A%23EXT-X-MEDIA%3ATYPE%3DSUBTITLES%2CGROUP-ID%3D%22subs%22%2CNAME%3D%22%C3%A6%C2%97%C2%A5%C3%A6%C2%9C%C2%AC%C3%A8%C2%AA%C2%9E%22%2CDEFAULT%3DNO%2CAUTOSELECT%3DYES%2CFORCED%3DNO%2CLANGUAGE%3D%22ja%22%2CCHARACTERISTICS%3D%22public.accessibility.transcribes-spoken-dialog%2C%20public.accessibility.describes-music-and-sound%22%2CURI%3D%22https%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fsubtitles%2Fjpn%2Fprog_index.m3u8%22%0D%0A%23EXT-X-MEDIA%3ATYPE%3DSUBTITLES%2CGROUP-ID%3D%22subs%22%2CNAME%3D%22%C3%A6%C2%97%C2%A5%C3%A6%C2%9C%C2%AC%C3%A8%C2%AA%C2%9E%20%28Forced%29%22%2CDEFAULT%3DNO%2CAUTOSELECT%3DNO%2CFORCED%3DYES%2CLANGUAGE%3D%22ja%22%2CURI%3D%22https%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fsubtitles%2Fjpn_forced%2Fprog_index.m3u8%22%0D%0A%0D%0A%0D%0A%23EXT-X-STREAM-INF%3ABANDWIDTH%3D263851%2CCODECS%3D%22mp4a.40.2%2C%20avc1.4d400d%22%2CRESOLUTION%3D416x234%2CAUDIO%3D%22bipbop_audio%22%2CSUBTITLES%3D%22subs%22%0D%0Ahttps%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fgear1%2Fprog_index.m3u8%0D%0A%0D%0A%23EXT-X-STREAM-INF%3ABANDWIDTH%3D577610%2CCODECS%3D%22mp4a.40.2%2C%20avc1.4d401e%22%2CRESOLUTION%3D640x360%2CAUDIO%3D%22bipbop_audio%22%2CSUBTITLES%3D%22subs%22%0D%0Ahttps%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fgear2%2Fprog_index.m3u8%0D%0A%0D%0A%23EXT-X-STREAM-INF%3ABANDWIDTH%3D915905%2CCODECS%3D%22mp4a.40.2%2C%20avc1.4d401f%22%2CRESOLUTION%3D960x540%2CAUDIO%3D%22bipbop_audio%22%2CSUBTITLES%3D%22subs%22%0D%0Ahttps%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fgear3%2Fprog_index.m3u8%0D%0A%0D%0A%23EXT-X-STREAM-INF%3ABANDWIDTH%3D1030138%2CCODECS%3D%22mp4a.40.2%2C%20avc1.4d401f%22%2CRESOLUTION%3D1280x720%2CAUDIO%3D%22bipbop_audio%22%2CSUBTITLES%3D%22subs%22%0D%0Ahttps%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fgear4%2Fprog_index.m3u8%0D%0A%0D%0A%23EXT-X-STREAM-INF%3ABANDWIDTH%3D1924009%2CCODECS%3D%22mp4a.40.2%2C%20avc1.4d401f%22%2CRESOLUTION%3D1920x1080%2CAUDIO%3D%22bipbop_audio%22%2CSUBTITLES%3D%22subs%22%0D%0Ahttps%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fgear5%2Fprog_index.m3u8%0D%0A%0D%0A%23EXT-X-STREAM-INF%3ABANDWIDTH%3D41457%2CCODECS%3D%22mp4a.40.2%22%2CAUDIO%3D%22bipbop_audio%22%2CSUBTITLES%3D%22subs%22%0D%0Ahttps%3A%2F%2Fd2zihajmogu5jn.cloudfront.net%2Fbipbop-advanced%2Fgear0%2Fprog_index.m3u8'; -QUnit[testFn]('hls data uri', function(assert) { +QUnit.test('hls data uri', function(assert) { const done = assert.async(); const player = this.player; @@ -488,7 +481,7 @@ QUnit[testFn]('hls data uri', function(assert) { const dashDataUri = 'data:application/dash+xml;charset=utf-8,%3CMPD%20mediaPresentationDuration=%22PT634.566S%22%20minBufferTime=%22PT2.00S%22%20profiles=%22urn:hbbtv:dash:profile:isoff-live:2012,urn:mpeg:dash:profile:isoff-live:2011%22%20type=%22static%22%20xmlns=%22urn:mpeg:dash:schema:mpd:2011%22%20xmlns:xsi=%22http://www.w3.org/2001/XMLSchema-instance%22%20xsi:schemaLocation=%22urn:mpeg:DASH:schema:MPD:2011%20DASH-MPD.xsd%22%3E%20%3CBaseURL%3Ehttps://dash.akamaized.net/akamai/bbb_30fps/%3C/BaseURL%3E%20%3CPeriod%3E%20%20%3CAdaptationSet%20mimeType=%22video/mp4%22%20contentType=%22video%22%20subsegmentAlignment=%22true%22%20subsegmentStartsWithSAP=%221%22%20par=%2216:9%22%3E%20%20%20%3CSegmentTemplate%20duration=%22120%22%20timescale=%2230%22%20media=%22$RepresentationID$/$RepresentationID$_$Number$.m4v%22%20startNumber=%221%22%20initialization=%22$RepresentationID$/$RepresentationID$_0.m4v%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_1024x576_2500k%22%20codecs=%22avc1.64001f%22%20bandwidth=%223134488%22%20width=%221024%22%20height=%22576%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_1280x720_4000k%22%20codecs=%22avc1.64001f%22%20bandwidth=%224952892%22%20width=%221280%22%20height=%22720%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_1920x1080_8000k%22%20codecs=%22avc1.640028%22%20bandwidth=%229914554%22%20width=%221920%22%20height=%221080%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_320x180_200k%22%20codecs=%22avc1.64000d%22%20bandwidth=%22254320%22%20width=%22320%22%20height=%22180%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_320x180_400k%22%20codecs=%22avc1.64000d%22%20bandwidth=%22507246%22%20width=%22320%22%20height=%22180%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_480x270_600k%22%20codecs=%22avc1.640015%22%20bandwidth=%22759798%22%20width=%22480%22%20height=%22270%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_640x360_1000k%22%20codecs=%22avc1.64001e%22%20bandwidth=%221254758%22%20width=%22640%22%20height=%22360%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_640x360_800k%22%20codecs=%22avc1.64001e%22%20bandwidth=%221013310%22%20width=%22640%22%20height=%22360%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_768x432_1500k%22%20codecs=%22avc1.64001e%22%20bandwidth=%221883700%22%20width=%22768%22%20height=%22432%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_30fps_3840x2160_12000k%22%20codecs=%22avc1.640033%22%20bandwidth=%2214931538%22%20width=%223840%22%20height=%222160%22%20frameRate=%2230%22%20sar=%221:1%22%20scanType=%22progressive%22/%3E%20%20%3C/AdaptationSet%3E%20%20%3CAdaptationSet%20mimeType=%22audio/mp4%22%20contentType=%22audio%22%20subsegmentAlignment=%22true%22%20subsegmentStartsWithSAP=%221%22%3E%20%20%20%3CAccessibility%20schemeIdUri=%22urn:tva:metadata:cs:AudioPurposeCS:2007%22%20value=%226%22/%3E%20%20%20%3CRole%20schemeIdUri=%22urn:mpeg:dash:role:2011%22%20value=%22main%22/%3E%20%20%20%3CSegmentTemplate%20duration=%22192512%22%20timescale=%2248000%22%20media=%22$RepresentationID$/$RepresentationID$_$Number$.m4a%22%20startNumber=%221%22%20initialization=%22$RepresentationID$/$RepresentationID$_0.m4a%22/%3E%20%20%20%3CRepresentation%20id=%22bbb_a64k%22%20codecs=%22mp4a.40.5%22%20bandwidth=%2267071%22%20audioSamplingRate=%2248000%22%3E%20%20%20%20%3CAudioChannelConfiguration%20schemeIdUri=%22urn:mpeg:dash:23003:3:audio_channel_configuration:2011%22%20value=%222%22/%3E%20%20%20%3C/Representation%3E%20%20%3C/AdaptationSet%3E%20%3C/Period%3E%3C/MPD%3E'; -QUnit[testFn]('dash data uri', function(assert) { +QUnit.test('dash data uri', function(assert) { const done = assert.async(); const player = this.player; @@ -504,7 +497,7 @@ QUnit[testFn]('dash data uri', function(assert) { }); }); -QUnit[testFn]('dash manifest object', function(assert) { +QUnit.test('dash manifest object', function(assert) { const done = assert.async(); const player = this.player; @@ -519,7 +512,7 @@ QUnit[testFn]('dash manifest object', function(assert) { }); }); -QUnit[testFn]('hls manifest object', function(assert) { +QUnit.test('hls manifest object', function(assert) { const done = assert.async(); const player = this.player; diff --git a/test/playlist-controller.test.js b/test/playlist-controller.test.js index 48e138c5a..b70a0828d 100644 --- a/test/playlist-controller.test.js +++ b/test/playlist-controller.test.js @@ -59,10 +59,8 @@ const sharedHooks = { this.requests = this.env.requests; this.mse = useFakeMediaSource(); - if (!videojs.browser.IE_VERSION) { - this.oldDevicePixelRatio = window.devicePixelRatio; - window.devicePixelRatio = 1; - } + this.oldDevicePixelRatio = window.devicePixelRatio; + window.devicePixelRatio = 1; // force the HLS tech to run this.origSupportsNativeHls = videojs.Vhs.supportsNativeHls; @@ -638,7 +636,7 @@ QUnit.test('seeks in place for fast quality switch on non-IE/Edge browsers', fun seeks++; }); - let timeBeforeSwitch = this.player.currentTime(); + const timeBeforeSwitch = this.player.currentTime(); // mock buffered values so removes are processed segmentLoader.sourceUpdater_.audioBuffer.buffered = createTimeRanges([[0, 10]]); @@ -650,10 +648,6 @@ QUnit.test('seeks in place for fast quality switch on non-IE/Edge browsers', fun segmentLoader.sourceUpdater_.videoBuffer.trigger('updateend'); this.clock.tick(1); - // we seek an additional 0.04s on edge and ie - if (videojs.browser.IS_EDGE || videojs.browser.IE_VERSION) { - timeBeforeSwitch += 0.04; - } assert.equal( this.player.currentTime(), timeBeforeSwitch, @@ -836,120 +830,6 @@ QUnit.test('demuxed timeToLoadedData, mediaAppends, appendsToLoadedData stats', }); }); -QUnit.test('seeks forward 0.04 sec for fast quality switch on Edge', function(assert) { - const oldIEVersion = videojs.browser.IE_VERSION; - const oldIsEdge = videojs.browser.IS_EDGE; - let seeks = 0; - - this.playlistController.mediaSource.trigger('sourceopen'); - // main - this.standardXHRResponse(this.requests.shift()); - // media - this.standardXHRResponse(this.requests.shift()); - - const segmentLoader = this.playlistController.mainSegmentLoader_; - - return requestAndAppendSegment({ - request: this.requests.shift(), - segmentLoader, - clock: this.clock - }).then(() => { - // media is changed - this.playlistController.selectPlaylist = () => { - const playlists = this.playlistController.main().playlists; - const currentPlaylist = this.playlistController.media(); - - return playlists.find((playlist) => playlist !== currentPlaylist); - }; - - this.player.tech_.on('seeking', function() { - seeks++; - }); - - const timeBeforeSwitch = this.player.currentTime(); - - videojs.browser.IE_VERSION = null; - videojs.browser.IS_EDGE = true; - - // mock buffered values so removes are processed - segmentLoader.sourceUpdater_.audioBuffer.buffered = createTimeRanges([[0, 10]]); - segmentLoader.sourceUpdater_.videoBuffer.buffered = createTimeRanges([[0, 10]]); - - this.playlistController.fastQualityChange_(); - // trigger updateend to indicate the end of the remove operation - segmentLoader.sourceUpdater_.audioBuffer.trigger('updateend'); - segmentLoader.sourceUpdater_.videoBuffer.trigger('updateend'); - this.clock.tick(1); - - assert.equal( - this.player.currentTime(), - timeBeforeSwitch + 0.04, - 'seeks forward on fast quality switch' - ); - assert.equal(seeks, 1, 'seek event occurs on fast quality switch'); - - videojs.browser.IE_VERSION = oldIEVersion; - videojs.browser.IS_EDGE = oldIsEdge; - }); -}); - -QUnit.test('seeks forward 0.04 sec for fast quality switch on IE', function(assert) { - const oldIEVersion = videojs.browser.IE_VERSION; - const oldIsEdge = videojs.browser.IS_EDGE; - let seeks = 0; - - this.playlistController.mediaSource.trigger('sourceopen'); - // main - this.standardXHRResponse(this.requests.shift()); - // media - this.standardXHRResponse(this.requests.shift()); - - const segmentLoader = this.playlistController.mainSegmentLoader_; - - return requestAndAppendSegment({ - request: this.requests.shift(), - segmentLoader, - clock: this.clock - }).then(() => { - // media is changed - this.playlistController.selectPlaylist = () => { - const playlists = this.playlistController.main().playlists; - const currentPlaylist = this.playlistController.media(); - - return playlists.find((playlist) => playlist !== currentPlaylist); - }; - - this.player.tech_.on('seeking', function() { - seeks++; - }); - - const timeBeforeSwitch = this.player.currentTime(); - - videojs.browser.IE_VERSION = 11; - videojs.browser.IS_EDGE = false; - - // mock buffered values so removes are processed - segmentLoader.sourceUpdater_.audioBuffer.buffered = createTimeRanges([[0, 10]]); - segmentLoader.sourceUpdater_.videoBuffer.buffered = createTimeRanges([[0, 10]]); - - this.playlistController.fastQualityChange_(); - // trigger updateend to indicate the end of the remove operation - segmentLoader.sourceUpdater_.audioBuffer.trigger('updateend'); - segmentLoader.sourceUpdater_.videoBuffer.trigger('updateend'); - this.clock.tick(1); - - assert.equal( - this.player.currentTime(), - timeBeforeSwitch + 0.04, - 'seeks forward on fast quality switch' - ); - assert.equal(seeks, 1, 'seek event occurs on fast quality switch'); - - videojs.browser.IE_VERSION = oldIEVersion; - videojs.browser.IS_EDGE = oldIsEdge; - }); -}); - QUnit.test('audio segment loader is reset on audio track change', function(assert) { this.requests.length = 0; this.player.dispose(); @@ -5040,6 +4920,25 @@ QUnit.test('playlist codecs take priority over others', function(assert) { assert.deepEqual(codecs, {video: 'avc1.4b400d', audio: 'mp4a.40.20'}, 'codecs returned'); }); +QUnit.test('Current pending segment\'s playlist codecs take priority over others', function(assert) { + this.contentSetup({ + mainStartingMedia: {videoCodec: 'avc1.4c400d', hasVideo: true, hasAudio: false}, + audioStartingMedia: {audioCodec: 'mp4a.40.5', hasVideo: false, hasAudio: true}, + mainPlaylist: {attributes: {CODECS: 'avc1.4b400d', AUDIO: 'low-quality'}}, + audioPlaylist: {attributes: {CODECS: 'mp4a.40.20'}} + }); + + const originalGetPendingSegmentPlaylist = this.pc.mainSegmentLoader_.getPendingSegmentPlaylist.bind(this.pc.mainSegmentLoader_); + + this.pc.mainSegmentLoader_.getPendingSegmentPlaylist = () => ({attributes: {CODECS: 'avc1.64001f', AUDIO: 'low-quality'}}); + + const codecs = this.pc.getCodecsOrExclude_(); + + assert.deepEqual(this.exclusionList, [], 'did not blacklist anything'); + assert.deepEqual(codecs, {video: 'avc1.64001f', audio: 'mp4a.40.20'}, 'codecs returned'); + this.pc.mainSegmentLoader_.getPendingSegmentPlaylist = originalGetPendingSegmentPlaylist; +}); + QUnit.test('uses default codecs if no codecs are found', function(assert) { this.contentSetup({ mainStartingMedia: {hasVideo: true, hasAudio: false}, @@ -5071,6 +4970,27 @@ QUnit.test('excludes playlist without detected audio/video', function(assert) { assert.deepEqual(codecs, void 0, 'no codecs returned'); }); +QUnit.test('excludes current pending segment\'s playlist without detected audio/video', function(assert) { + this.contentSetup({ + mainStartingMedia: {}, + audioStartingMedia: {}, + mainPlaylist: {attributes: {}} + }); + + const originalGetPendingSegmentPlaylist = this.pc.mainSegmentLoader_.getPendingSegmentPlaylist.bind(this.pc.mainSegmentLoader_); + + this.pc.mainSegmentLoader_.getPendingSegmentPlaylist = () => ({attributes: {CODECS: ''}}); + const codecs = this.pc.getCodecsOrExclude_(); + + assert.deepEqual(this.exclusionList, [{ + playlistExclusionDuration: Infinity, + playlistToExclude: {attributes: {CODECS: ''}}, + error: { message: 'Could not determine codecs for playlist.' } + }], 'excluded playlist'); + assert.deepEqual(codecs, void 0, 'no codecs returned'); + this.pc.mainSegmentLoader_.getPendingSegmentPlaylist = originalGetPendingSegmentPlaylist; +}); + QUnit.test('excludes unsupported muxer codecs for ts', function(assert) { this.contentSetup({ mainStartingMedia: { diff --git a/test/playlist-loader.test.js b/test/playlist-loader.test.js index 96f67a801..c642db048 100644 --- a/test/playlist-loader.test.js +++ b/test/playlist-loader.test.js @@ -1,5 +1,4 @@ import QUnit from 'qunit'; -import videojs from 'video.js'; import { default as PlaylistLoader, updateSegments, @@ -2422,435 +2421,433 @@ QUnit.module('Playlist Loader', function(hooks) { assert.equal(this.requests.length, 1, 'playlist re-requested'); }); - if (!videojs.browser.IE_VERSION) { - QUnit.module('llhls', { - beforeEach() { - this.fakeVhs.options_ = {llhls: true}; - this.loader = new PlaylistLoader('http://example.com/media.m3u8', this.fakeVhs); + QUnit.module('llhls', { + beforeEach() { + this.fakeVhs.options_ = {llhls: true}; + this.loader = new PlaylistLoader('http://example.com/media.m3u8', this.fakeVhs); - this.loader.load(); - - }, - afterEach() { - this.loader.dispose(); - } - }); - - QUnit.test('#EXT-X-SKIP does not add initial empty segments', function(assert) { - this.requests.shift().respond( - 200, null, - '#EXTM3U\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n' + - '#EXT-X-SKIP:SKIPPED-SEGMENTS=10\n' + - '#EXTINF:2\n' + - 'low-1.ts\n' - ); - assert.equal(this.loader.media().segments.length, 1, 'only 1 segment'); - }); - - QUnit.test('#EXT-X-SKIP merges skipped segments', function(assert) { - let playlist = - '#EXTM3U\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n'; - - for (let i = 0; i < 10; i++) { - playlist += '#EXTINF:2\n'; - playlist += `segment-${i}.ts\n`; - } - - this.requests.shift().respond(200, null, playlist); - assert.equal(this.loader.media().segments.length, 10, '10 segments'); - - this.loader.trigger('mediaupdatetimeout'); - - const skippedPlaylist = - '#EXTM3U\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n' + - '#EXT-X-SKIP:SKIPPED-SEGMENTS=10\n' + - '#EXTINF:2\n' + - 'segment-10.ts\n'; - - this.requests.shift().respond(200, null, skippedPlaylist); - - assert.equal(this.loader.media().segments.length, 11, '11 segments'); - - this.loader.media().segments.forEach(function(s, i) { - if (i < 10) { - assert.ok(s.hasOwnProperty('skipped'), 'has skipped property'); - assert.false(s.skipped, 'skipped property is false'); - } - - assert.equal(s.uri, `segment-${i}.ts`, 'segment uri as expected'); - }); - - this.loader.trigger('mediaupdatetimeout'); - - const skippedPlaylist2 = - '#EXTM3U\n' + - '#EXT-X-MEDIA-SEQUENCE:1\n' + - '#EXT-X-SKIP:SKIPPED-SEGMENTS=10\n' + - '#EXTINF:2\n' + - 'segment-11.ts\n'; - - this.requests.shift().respond(200, null, skippedPlaylist2); + this.loader.load(); - this.loader.media().segments.forEach(function(s, i) { - if (i < 10) { - assert.ok(s.hasOwnProperty('skipped'), 'has skipped property'); - assert.false(s.skipped, 'skipped property is false'); - } + }, + afterEach() { + this.loader.dispose(); + } + }); - assert.equal(s.uri, `segment-${i + 1}.ts`, 'segment uri as expected'); - }); - }); + QUnit.test('#EXT-X-SKIP does not add initial empty segments', function(assert) { + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXT-X-SKIP:SKIPPED-SEGMENTS=10\n' + + '#EXTINF:2\n' + + 'low-1.ts\n' + ); + assert.equal(this.loader.media().segments.length, 1, 'only 1 segment'); + }); - QUnit.test('#EXT-X-PRELOAD with parts to added to segment list', function(assert) { - this.requests.shift().respond( - 200, null, - '#EXTM3U\n' + - '#EXT-X-PART-INF:PART-TARGET=1\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n' + - '#EXTINF:2\n' + - 'low-1.ts\n' + - '#EXT-X-PART:URI="part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="part2.ts",DURATION=1\n' - ); - const media = this.loader.media(); + QUnit.test('#EXT-X-SKIP merges skipped segments', function(assert) { + let playlist = + '#EXTM3U\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n'; - assert.equal(media.segments.length, 2, '2 segments'); - assert.deepEqual( - media.preloadSegment, - media.segments[media.segments.length - 1], - 'last segment is preloadSegment' - ); - }); + for (let i = 0; i < 10; i++) { + playlist += '#EXTINF:2\n'; + playlist += `segment-${i}.ts\n`; + } - QUnit.test('#EXT-X-PRELOAD without parts not added to segment list', function(assert) { - this.requests.shift().respond( - 200, null, - '#EXTM3U\n' + - '#EXT-X-PART-INF:PART-TARGET=1\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n' + - '#EXTINF:2\n' + - 'low-1.ts\n' + - '#EXT-X-PRELOAD-HINT:TYPE="PART",URI="part1.ts"\n' - ); - const media = this.loader.media(); + this.requests.shift().respond(200, null, playlist); + assert.equal(this.loader.media().segments.length, 10, '10 segments'); - assert.equal(media.segments.length, 1, '1 segment'); - assert.notDeepEqual( - media.preloadSegment, - media.segments[media.segments.length - 1], - 'last segment is not preloadSegment' - ); - }); + this.loader.trigger('mediaupdatetimeout'); - QUnit.test('#EXT-X-PART added to segments', function(assert) { - this.requests.shift().respond( - 200, null, - '#EXTM3U\n' + - '#EXT-X-PART-INF:PART-TARGET=1\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n' + - '#EXTINF:2\n' + - 'segment0.ts\n' + - '#EXT-X-PART:URI="segment1-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment1-part2.ts",DURATION=1\n' + - 'segment1.ts\n' + - '#EXT-X-PART:URI="segment2-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment2-part2.ts",DURATION=1\n' + - 'segment2.ts\n' + - '#EXT-X-PART:URI="segment3-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment3-part2.ts",DURATION=1\n' + - 'segment3.ts\n' - ); - const segments = this.loader.media().segments; + const skippedPlaylist = + '#EXTM3U\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXT-X-SKIP:SKIPPED-SEGMENTS=10\n' + + '#EXTINF:2\n' + + 'segment-10.ts\n'; - assert.equal(segments.length, 4, '4 segments'); - assert.notOk(segments[0].parts, 'no parts for first segment'); - assert.equal(segments[1].parts.length, 2, 'parts for second segment'); - assert.equal(segments[2].parts.length, 2, 'parts for third segment'); - assert.equal(segments[3].parts.length, 2, 'parts for forth segment'); - }); + this.requests.shift().respond(200, null, skippedPlaylist); - QUnit.test('Adds _HLS_skip=YES to url when CAN-SKIP-UNTIL is set', function(assert) { - this.requests.shift().respond( - 200, null, - '#EXTM3U\n' + - '#EXT-X-PART-INF:PART-TARGET=1\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n' + - '#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=3\n' + - '#EXTINF:2\n' + - 'segment0.ts\n' + - '#EXTINF:2\n' + - 'segment1.ts\n' + - '#EXTINF:2\n' + - 'segment2.ts\n' + - '#EXTINF:2\n' + - 'segment3.ts\n' + - '#EXTINF:2\n' + - 'segment4.ts\n' + - '#EXTINF:2\n' + - 'segment5.ts\n' + - '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + - 'segment6.ts\n' + - '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + - 'segment7.ts\n' + - '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment8-part2.ts",DURATION=1\n' + - 'segment8.ts\n' - ); + assert.equal(this.loader.media().segments.length, 11, '11 segments'); - this.loader.trigger('mediaupdatetimeout'); + this.loader.media().segments.forEach(function(s, i) { + if (i < 10) { + assert.ok(s.hasOwnProperty('skipped'), 'has skipped property'); + assert.false(s.skipped, 'skipped property is false'); + } - assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_skip=YES'); + assert.equal(s.uri, `segment-${i}.ts`, 'segment uri as expected'); }); - QUnit.test('Adds _HLS_skip=v2 to url when CAN-SKIP-UNTIL/CAN-SKIP-DATERANGES is set', function(assert) { - this.requests.shift().respond( - 200, null, - '#EXTM3U\n' + - '#EXT-X-PART-INF:PART-TARGET=1\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n' + - '#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=3,CAN-SKIP-DATERANGES=YES\n' + - '#EXTINF:2\n' + - 'segment0.ts\n' + - '#EXTINF:2\n' + - 'segment1.ts\n' + - '#EXTINF:2\n' + - 'segment2.ts\n' + - '#EXTINF:2\n' + - 'segment3.ts\n' + - '#EXTINF:2\n' + - 'segment4.ts\n' + - '#EXTINF:2\n' + - 'segment5.ts\n' + - '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + - 'segment6.ts\n' + - '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + - 'segment7.ts\n' + - '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment8-part2.ts",DURATION=1\n' + - 'segment8.ts\n' - ); - - this.loader.trigger('mediaupdatetimeout'); + this.loader.trigger('mediaupdatetimeout'); - assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_skip=v2'); - }); + const skippedPlaylist2 = + '#EXTM3U\n' + + '#EXT-X-MEDIA-SEQUENCE:1\n' + + '#EXT-X-SKIP:SKIPPED-SEGMENTS=10\n' + + '#EXTINF:2\n' + + 'segment-11.ts\n'; - QUnit.test('Adds _HLS_part= and _HLS_msn= when we have a part preload hints and parts', function(assert) { - this.requests.shift().respond( - 200, null, - '#EXTM3U\n' + - '#EXT-X-PART-INF:PART-TARGET=1\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n' + - '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES\n' + - '#EXTINF:2\n' + - 'segment0.ts\n' + - '#EXTINF:2\n' + - 'segment1.ts\n' + - '#EXTINF:2\n' + - 'segment2.ts\n' + - '#EXTINF:2\n' + - 'segment3.ts\n' + - '#EXTINF:2\n' + - 'segment4.ts\n' + - '#EXTINF:2\n' + - 'segment5.ts\n' + - '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + - 'segment6.ts\n' + - '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + - 'segment7.ts\n' + - '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' + - '#EXT-X-PRELOAD-HINT:TYPE="PART",URI="segment8-part2.ts"\n' - ); + this.requests.shift().respond(200, null, skippedPlaylist2); - this.loader.trigger('mediaupdatetimeout'); + this.loader.media().segments.forEach(function(s, i) { + if (i < 10) { + assert.ok(s.hasOwnProperty('skipped'), 'has skipped property'); + assert.false(s.skipped, 'skipped property is false'); + } - assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_msn=8&_HLS_part=1'); + assert.equal(s.uri, `segment-${i + 1}.ts`, 'segment uri as expected'); }); + }); - QUnit.test('Adds _HLS_part= and _HLS_msn= when we have only a part preload hint', function(assert) { - this.requests.shift().respond( - 200, null, - '#EXTM3U\n' + - '#EXT-X-PART-INF:PART-TARGET=1\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n' + - '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES\n' + - '#EXTINF:2\n' + - 'segment0.ts\n' + - '#EXTINF:2\n' + - 'segment1.ts\n' + - '#EXTINF:2\n' + - 'segment2.ts\n' + - '#EXTINF:2\n' + - 'segment3.ts\n' + - '#EXTINF:2\n' + - 'segment4.ts\n' + - '#EXTINF:2\n' + - 'segment5.ts\n' + - '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + - 'segment6.ts\n' + - '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + - 'segment7.ts\n' + - '#EXT-X-PRELOAD-HINT:TYPE="PART",URI="segment8-part1.ts"\n' - ); - - this.loader.trigger('mediaupdatetimeout'); - - assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_msn=7&_HLS_part=0'); - }); + QUnit.test('#EXT-X-PRELOAD with parts to added to segment list', function(assert) { + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-PART-INF:PART-TARGET=1\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXTINF:2\n' + + 'low-1.ts\n' + + '#EXT-X-PART:URI="part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="part2.ts",DURATION=1\n' + ); + const media = this.loader.media(); - QUnit.test('does not add _HLS_part= when we have only a preload parts without preload hints', function(assert) { - this.requests.shift().respond( - 200, null, - '#EXTM3U\n' + - '#EXT-X-PART-INF:PART-TARGET=1\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n' + - '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES\n' + - '#EXTINF:2\n' + - 'segment0.ts\n' + - '#EXTINF:2\n' + - 'segment1.ts\n' + - '#EXTINF:2\n' + - 'segment2.ts\n' + - '#EXTINF:2\n' + - 'segment3.ts\n' + - '#EXTINF:2\n' + - 'segment4.ts\n' + - '#EXTINF:2\n' + - 'segment5.ts\n' + - '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + - 'segment6.ts\n' + - '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + - 'segment7.ts\n' + - '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' - ); + assert.equal(media.segments.length, 2, '2 segments'); + assert.deepEqual( + media.preloadSegment, + media.segments[media.segments.length - 1], + 'last segment is preloadSegment' + ); + }); - this.loader.trigger('mediaupdatetimeout'); + QUnit.test('#EXT-X-PRELOAD without parts not added to segment list', function(assert) { + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-PART-INF:PART-TARGET=1\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXTINF:2\n' + + 'low-1.ts\n' + + '#EXT-X-PRELOAD-HINT:TYPE="PART",URI="part1.ts"\n' + ); + const media = this.loader.media(); - assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_msn=8'); - }); + assert.equal(media.segments.length, 1, '1 segment'); + assert.notDeepEqual( + media.preloadSegment, + media.segments[media.segments.length - 1], + 'last segment is not preloadSegment' + ); + }); - QUnit.test('Adds only _HLS_msn= when we have segment info', function(assert) { - this.requests.shift().respond( - 200, null, - '#EXTM3U\n' + - '#EXT-X-PART-INF:PART-TARGET=1\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n' + - '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES\n' + - '#EXTINF:2\n' + - 'segment0.ts\n' + - '#EXTINF:2\n' + - 'segment1.ts\n' + - '#EXTINF:2\n' + - 'segment2.ts\n' + - '#EXTINF:2\n' + - 'segment3.ts\n' + - '#EXTINF:2\n' + - 'segment4.ts\n' + - '#EXTINF:2\n' + - 'segment5.ts\n' + - '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + - 'segment6.ts\n' + - '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + - 'segment7.ts\n' + - '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment8-part2.ts",DURATION=1\n' + - 'segment8.ts\n' - ); + QUnit.test('#EXT-X-PART added to segments', function(assert) { + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-PART-INF:PART-TARGET=1\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXTINF:2\n' + + 'segment0.ts\n' + + '#EXT-X-PART:URI="segment1-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment1-part2.ts",DURATION=1\n' + + 'segment1.ts\n' + + '#EXT-X-PART:URI="segment2-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment2-part2.ts",DURATION=1\n' + + 'segment2.ts\n' + + '#EXT-X-PART:URI="segment3-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment3-part2.ts",DURATION=1\n' + + 'segment3.ts\n' + ); + const segments = this.loader.media().segments; + + assert.equal(segments.length, 4, '4 segments'); + assert.notOk(segments[0].parts, 'no parts for first segment'); + assert.equal(segments[1].parts.length, 2, 'parts for second segment'); + assert.equal(segments[2].parts.length, 2, 'parts for third segment'); + assert.equal(segments[3].parts.length, 2, 'parts for forth segment'); + }); - this.loader.trigger('mediaupdatetimeout'); + QUnit.test('Adds _HLS_skip=YES to url when CAN-SKIP-UNTIL is set', function(assert) { + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-PART-INF:PART-TARGET=1\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=3\n' + + '#EXTINF:2\n' + + 'segment0.ts\n' + + '#EXTINF:2\n' + + 'segment1.ts\n' + + '#EXTINF:2\n' + + 'segment2.ts\n' + + '#EXTINF:2\n' + + 'segment3.ts\n' + + '#EXTINF:2\n' + + 'segment4.ts\n' + + '#EXTINF:2\n' + + 'segment5.ts\n' + + '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + + 'segment6.ts\n' + + '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + + 'segment7.ts\n' + + '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment8-part2.ts",DURATION=1\n' + + 'segment8.ts\n' + ); + + this.loader.trigger('mediaupdatetimeout'); + + assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_skip=YES'); + }); - assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_msn=9'); - }); + QUnit.test('Adds _HLS_skip=v2 to url when CAN-SKIP-UNTIL/CAN-SKIP-DATERANGES is set', function(assert) { + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-PART-INF:PART-TARGET=1\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=3,CAN-SKIP-DATERANGES=YES\n' + + '#EXTINF:2\n' + + 'segment0.ts\n' + + '#EXTINF:2\n' + + 'segment1.ts\n' + + '#EXTINF:2\n' + + 'segment2.ts\n' + + '#EXTINF:2\n' + + 'segment3.ts\n' + + '#EXTINF:2\n' + + 'segment4.ts\n' + + '#EXTINF:2\n' + + 'segment5.ts\n' + + '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + + 'segment6.ts\n' + + '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + + 'segment7.ts\n' + + '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment8-part2.ts",DURATION=1\n' + + 'segment8.ts\n' + ); + + this.loader.trigger('mediaupdatetimeout'); + + assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_skip=v2'); + }); - QUnit.test('can add all query directives', function(assert) { - this.requests.shift().respond( - 200, null, - '#EXTM3U\n' + - '#EXT-X-PART-INF:PART-TARGET=1\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n' + - '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=3\n' + - '#EXTINF:2\n' + - 'segment0.ts\n' + - '#EXTINF:2\n' + - 'segment1.ts\n' + - '#EXTINF:2\n' + - 'segment2.ts\n' + - '#EXTINF:2\n' + - 'segment3.ts\n' + - '#EXTINF:2\n' + - 'segment4.ts\n' + - '#EXTINF:2\n' + - 'segment5.ts\n' + - '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + - 'segment6.ts\n' + - '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + - 'segment7.ts\n' + - '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' + - '#EXT-X-PRELOAD-HINT:TYPE="PART",URI="segment8-part2.ts"\n' - ); + QUnit.test('Adds _HLS_part= and _HLS_msn= when we have a part preload hints and parts', function(assert) { + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-PART-INF:PART-TARGET=1\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES\n' + + '#EXTINF:2\n' + + 'segment0.ts\n' + + '#EXTINF:2\n' + + 'segment1.ts\n' + + '#EXTINF:2\n' + + 'segment2.ts\n' + + '#EXTINF:2\n' + + 'segment3.ts\n' + + '#EXTINF:2\n' + + 'segment4.ts\n' + + '#EXTINF:2\n' + + 'segment5.ts\n' + + '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + + 'segment6.ts\n' + + '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + + 'segment7.ts\n' + + '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' + + '#EXT-X-PRELOAD-HINT:TYPE="PART",URI="segment8-part2.ts"\n' + ); + + this.loader.trigger('mediaupdatetimeout'); + + assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_msn=8&_HLS_part=1'); + }); - this.loader.trigger('mediaupdatetimeout'); + QUnit.test('Adds _HLS_part= and _HLS_msn= when we have only a part preload hint', function(assert) { + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-PART-INF:PART-TARGET=1\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES\n' + + '#EXTINF:2\n' + + 'segment0.ts\n' + + '#EXTINF:2\n' + + 'segment1.ts\n' + + '#EXTINF:2\n' + + 'segment2.ts\n' + + '#EXTINF:2\n' + + 'segment3.ts\n' + + '#EXTINF:2\n' + + 'segment4.ts\n' + + '#EXTINF:2\n' + + 'segment5.ts\n' + + '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + + 'segment6.ts\n' + + '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + + 'segment7.ts\n' + + '#EXT-X-PRELOAD-HINT:TYPE="PART",URI="segment8-part1.ts"\n' + ); + + this.loader.trigger('mediaupdatetimeout'); + + assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_msn=7&_HLS_part=0'); + }); - assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_skip=YES&_HLS_msn=8&_HLS_part=1'); - }); + QUnit.test('does not add _HLS_part= when we have only a preload parts without preload hints', function(assert) { + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-PART-INF:PART-TARGET=1\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES\n' + + '#EXTINF:2\n' + + 'segment0.ts\n' + + '#EXTINF:2\n' + + 'segment1.ts\n' + + '#EXTINF:2\n' + + 'segment2.ts\n' + + '#EXTINF:2\n' + + 'segment3.ts\n' + + '#EXTINF:2\n' + + 'segment4.ts\n' + + '#EXTINF:2\n' + + 'segment5.ts\n' + + '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + + 'segment6.ts\n' + + '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + + 'segment7.ts\n' + + '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' + ); + + this.loader.trigger('mediaupdatetimeout'); + + assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_msn=8'); + }); - QUnit.test('works with existing query directives', function(assert) { - // clear existing requests - this.requests.length = 0; + QUnit.test('Adds only _HLS_msn= when we have segment info', function(assert) { + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-PART-INF:PART-TARGET=1\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES\n' + + '#EXTINF:2\n' + + 'segment0.ts\n' + + '#EXTINF:2\n' + + 'segment1.ts\n' + + '#EXTINF:2\n' + + 'segment2.ts\n' + + '#EXTINF:2\n' + + 'segment3.ts\n' + + '#EXTINF:2\n' + + 'segment4.ts\n' + + '#EXTINF:2\n' + + 'segment5.ts\n' + + '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + + 'segment6.ts\n' + + '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + + 'segment7.ts\n' + + '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment8-part2.ts",DURATION=1\n' + + 'segment8.ts\n' + ); + + this.loader.trigger('mediaupdatetimeout'); + + assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_msn=9'); + }); - this.loader.dispose(); - this.loader = new PlaylistLoader('http://example.com/media.m3u8?foo=test', this.fakeVhs); + QUnit.test('can add all query directives', function(assert) { + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-PART-INF:PART-TARGET=1\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=3\n' + + '#EXTINF:2\n' + + 'segment0.ts\n' + + '#EXTINF:2\n' + + 'segment1.ts\n' + + '#EXTINF:2\n' + + 'segment2.ts\n' + + '#EXTINF:2\n' + + 'segment3.ts\n' + + '#EXTINF:2\n' + + 'segment4.ts\n' + + '#EXTINF:2\n' + + 'segment5.ts\n' + + '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + + 'segment6.ts\n' + + '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + + 'segment7.ts\n' + + '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' + + '#EXT-X-PRELOAD-HINT:TYPE="PART",URI="segment8-part2.ts"\n' + ); + + this.loader.trigger('mediaupdatetimeout'); + + assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?_HLS_skip=YES&_HLS_msn=8&_HLS_part=1'); + }); - this.loader.load(); + QUnit.test('works with existing query directives', function(assert) { + // clear existing requests + this.requests.length = 0; - this.requests.shift().respond( - 200, null, - '#EXTM3U\n' + - '#EXT-X-PART-INF:PART-TARGET=1\n' + - '#EXT-X-MEDIA-SEQUENCE:0\n' + - '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=3\n' + - '#EXTINF:2\n' + - 'segment0.ts\n' + - '#EXTINF:2\n' + - 'segment1.ts\n' + - '#EXTINF:2\n' + - 'segment2.ts\n' + - '#EXTINF:2\n' + - 'segment3.ts\n' + - '#EXTINF:2\n' + - 'segment4.ts\n' + - '#EXTINF:2\n' + - 'segment5.ts\n' + - '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + - 'segment6.ts\n' + - '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + - 'segment7.ts\n' + - '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' + - '#EXT-X-PRELOAD-HINT:TYPE="PART",URI="segment8-part2.ts"\n' - ); + this.loader.dispose(); + this.loader = new PlaylistLoader('http://example.com/media.m3u8?foo=test', this.fakeVhs); - this.loader.trigger('mediaupdatetimeout'); + this.loader.load(); - assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?foo=test&_HLS_skip=YES&_HLS_msn=8&_HLS_part=1'); - }); - } + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-PART-INF:PART-TARGET=1\n' + + '#EXT-X-MEDIA-SEQUENCE:0\n' + + '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=3\n' + + '#EXTINF:2\n' + + 'segment0.ts\n' + + '#EXTINF:2\n' + + 'segment1.ts\n' + + '#EXTINF:2\n' + + 'segment2.ts\n' + + '#EXTINF:2\n' + + 'segment3.ts\n' + + '#EXTINF:2\n' + + 'segment4.ts\n' + + '#EXTINF:2\n' + + 'segment5.ts\n' + + '#EXT-X-PART:URI="segment6-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment6-part2.ts",DURATION=1\n' + + 'segment6.ts\n' + + '#EXT-X-PART:URI="segment7-part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="segment7-part2.ts",DURATION=1\n' + + 'segment7.ts\n' + + '#EXT-X-PART:URI="segment8-part1.ts",DURATION=1\n' + + '#EXT-X-PRELOAD-HINT:TYPE="PART",URI="segment8-part2.ts"\n' + ); + + this.loader.trigger('mediaupdatetimeout'); + + assert.equal(this.requests[0].uri, 'http://example.com/media.m3u8?foo=test&_HLS_skip=YES&_HLS_msn=8&_HLS_part=1'); + }); }); diff --git a/test/playlist.test.js b/test/playlist.test.js index 9e6f2c6d6..a02c4f9cd 100644 --- a/test/playlist.test.js +++ b/test/playlist.test.js @@ -2,7 +2,6 @@ import Playlist from '../src/playlist'; import PlaylistLoader from '../src/playlist-loader'; import QUnit from 'qunit'; import xhrFactory from '../src/xhr'; -import videojs from 'video.js'; import { useFakeEnvironment } from './test-helpers'; // needed for plugin registration import '../src/videojs-http-streaming'; @@ -1463,56 +1462,54 @@ QUnit.module('Playlist', function() { } ); - if (!videojs.browser.IE_VERSION) { - QUnit.test('can return a partIndex', function(assert) { - this.fakeVhs.options_ = {llhls: true}; - const loader = new PlaylistLoader('media.m3u8', this.fakeVhs); + QUnit.test('can return a partIndex', function(assert) { + this.fakeVhs.options_ = {llhls: true}; + const loader = new PlaylistLoader('media.m3u8', this.fakeVhs); - loader.load(); + loader.load(); - this.requests.shift().respond( - 200, null, - '#EXTM3U\n' + - '#EXT-X-MEDIA-SEQUENCE:1001\n' + - '#EXTINF:4,\n' + - '1001.ts\n' + - '#EXTINF:5,\n' + - '1002.ts\n' + - '#EXT-X-PART:URI="1003.part1.ts",DURATION=1\n' + - '#EXT-X-PART:URI="1003.part2.ts",DURATION=1\n' + - '#EXT-X-PART:URI="1003.part3.ts",DURATION=1\n' + - '#EXT-X-PRELOAD-HINT:TYPE="PART",URI="1003.part4.ts"\n' - ); + this.requests.shift().respond( + 200, null, + '#EXTM3U\n' + + '#EXT-X-MEDIA-SEQUENCE:1001\n' + + '#EXTINF:4,\n' + + '1001.ts\n' + + '#EXTINF:5,\n' + + '1002.ts\n' + + '#EXT-X-PART:URI="1003.part1.ts",DURATION=1\n' + + '#EXT-X-PART:URI="1003.part2.ts",DURATION=1\n' + + '#EXT-X-PART:URI="1003.part3.ts",DURATION=1\n' + + '#EXT-X-PRELOAD-HINT:TYPE="PART",URI="1003.part4.ts"\n' + ); - const media = loader.media(); + const media = loader.media(); - this.defaults = { - playlist: media, - currentTime: 0, - startingSegmentIndex: 0, - startingPartIndex: null, - startTime: 0 - }; + this.defaults = { + playlist: media, + currentTime: 0, + startingSegmentIndex: 0, + startingPartIndex: null, + startTime: 0 + }; - assert.deepEqual( - this.getMediaInfoForTime({currentTime: 10, startTime: 0}), - {segmentIndex: 2, startTime: 9, partIndex: 0}, - 'returns expected part/segment' - ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 10, startTime: 0}), + {segmentIndex: 2, startTime: 9, partIndex: 0}, + 'returns expected part/segment' + ); - assert.deepEqual( - this.getMediaInfoForTime({currentTime: 11, startTime: 0}), - {segmentIndex: 2, startTime: 10, partIndex: 1}, - 'returns expected part/segment' - ); + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 11, startTime: 0}), + {segmentIndex: 2, startTime: 10, partIndex: 1}, + 'returns expected part/segment' + ); - assert.deepEqual( - this.getMediaInfoForTime({currentTime: 11, segmentIndex: -15}), - {segmentIndex: 2, startTime: 10, partIndex: 1}, - 'returns expected part/segment' - ); - }); - } + assert.deepEqual( + this.getMediaInfoForTime({currentTime: 11, segmentIndex: -15}), + {segmentIndex: 2, startTime: 10, partIndex: 1}, + 'returns expected part/segment' + ); + }); QUnit.test('liveEdgeDelay works as expected', function(assert) { const media = { diff --git a/test/sanity.test.js b/test/sanity.test.js index 0f8e29c18..d45cb98e8 100644 --- a/test/sanity.test.js +++ b/test/sanity.test.js @@ -11,11 +11,7 @@ QUnit.test('the environment is sane', function(assert) { assert.strictEqual(typeof sinon, 'object', 'sinon exists'); assert.strictEqual(typeof videojs, 'function', 'videojs exists'); assert.strictEqual(typeof window.MediaSource, 'function', 'MediaSource is a function'); - if (videojs.browser.IE_VERSION) { - assert.strictEqual(typeof window.URL, 'object', 'URL is an object'); - } else { - assert.strictEqual(typeof window.URL, 'function', 'URL is a function'); - } + assert.strictEqual(typeof window.URL, 'function', 'URL is a function'); assert.strictEqual(typeof videojs.Vhs, 'object', 'Vhs is an object'); assert.strictEqual( typeof videojs.VhsSourceHandler, diff --git a/test/source-updater.test.js b/test/source-updater.test.js index d3c042328..4ca5d57f2 100644 --- a/test/source-updater.test.js +++ b/test/source-updater.test.js @@ -9,21 +9,9 @@ import { QUOTA_EXCEEDED_ERR } from '../src/error-codes'; import {createTimeRanges} from '../src/util/vjs-compat'; const checkInitialDuration = function({duration}) { - // ie sometimes sets duration to infinity earlier then expected - if (videojs.browser.IS_EDGE || videojs.browser.IE_VERSION) { - QUnit.assert.ok(Number.isNaN(duration) || !Number.isFinite(duration), 'starting duration as expected'); - } else { - QUnit.assert.ok(Number.isNaN(duration), 'starting duration as expected'); - } + QUnit.assert.ok(Number.isNaN(duration), 'starting duration as expected'); }; -let testOrSkip = 'test'; - -// some tests just don't work reliably on ie11 or edge -if (videojs.browser.IS_EDGE || videojs.browser.IE_VERSION) { - testOrSkip = 'skip'; -} - const concatSegments = (...segments) => { let byteLength = segments.reduce((acc, cv) => { acc += cv.byteLength; @@ -962,7 +950,7 @@ QUnit.test( } ); -QUnit[testOrSkip]('setDuration waits for audio buffer to finish updating', function(assert) { +QUnit.test('setDuration waits for audio buffer to finish updating', function(assert) { const done = assert.async(); assert.expect(5); @@ -1013,50 +1001,48 @@ QUnit.test('setDuration waits for video buffer to finish updating', function(ass assert.ok(this.sourceUpdater.updating(), 'updating during appends'); }); -if (!videojs.browser.IS_EDGE) { - QUnit.test( - 'setDuration waits for both audio and video buffers to finish updating', - function(assert) { - const done = assert.async(); - let appendsFinished = 0; +QUnit.test( + 'setDuration waits for both audio and video buffers to finish updating', + function(assert) { + const done = assert.async(); + let appendsFinished = 0; - assert.expect(7); + assert.expect(7); - this.sourceUpdater.createSourceBuffers({ - audio: 'mp4a.40.2', - video: 'avc1.4D001E' - }); + this.sourceUpdater.createSourceBuffers({ + audio: 'mp4a.40.2', + video: 'avc1.4D001E' + }); - assert.notOk(this.sourceUpdater.updating(), 'not updating by default'); + assert.notOk(this.sourceUpdater.updating(), 'not updating by default'); - const checkDuration = () => { - // duration is set to infinity if content is appended before an explicit duration is - // set https://w3c.github.io/media-source/#sourcebuffer-init-segment-received - assert.equal(this.mediaSource.duration, Infinity, 'duration not set on media source'); + const checkDuration = () => { + // duration is set to infinity if content is appended before an explicit duration is + // set https://w3c.github.io/media-source/#sourcebuffer-init-segment-received + assert.equal(this.mediaSource.duration, Infinity, 'duration not set on media source'); - if (appendsFinished === 0) { - // try to set the duration while one of the buffers is still updating, this should - // happen after the other setDuration call - this.sourceUpdater.setDuration(12, () => { - assert.equal(this.mediaSource.duration, 12, 'set duration on media source'); - done(); - }); - } + if (appendsFinished === 0) { + // try to set the duration while one of the buffers is still updating, this should + // happen after the other setDuration call + this.sourceUpdater.setDuration(12, () => { + assert.equal(this.mediaSource.duration, 12, 'set duration on media source'); + done(); + }); + } - appendsFinished++; - }; + appendsFinished++; + }; - this.sourceUpdater.appendBuffer({type: 'video', bytes: mp4VideoTotal()}, checkDuration); - this.sourceUpdater.appendBuffer({type: 'audio', bytes: mp4AudioTotal()}, checkDuration); - this.sourceUpdater.setDuration(11, () => { - assert.equal(this.mediaSource.duration, 11, 'set duration on media source'); - }); + this.sourceUpdater.appendBuffer({type: 'video', bytes: mp4VideoTotal()}, checkDuration); + this.sourceUpdater.appendBuffer({type: 'audio', bytes: mp4AudioTotal()}, checkDuration); + this.sourceUpdater.setDuration(11, () => { + assert.equal(this.mediaSource.duration, 11, 'set duration on media source'); + }); - checkInitialDuration(this.mediaSource); - assert.ok(this.sourceUpdater.updating(), 'updating during appends'); - } - ); -} + checkInitialDuration(this.mediaSource); + assert.ok(this.sourceUpdater.updating(), 'updating during appends'); + } +); QUnit.test( 'setDuration blocks audio and video queue entries until it finishes', @@ -1318,7 +1304,7 @@ QUnit.test('dispose removes sourceopen listener', function(assert) { }); }); -QUnit[testOrSkip]('audio appends are delayed until video append for the first append', function(assert) { +QUnit.test('audio appends are delayed until video append for the first append', function(assert) { const done = assert.async(); let audioAppend = false; let videoAppend = false; diff --git a/test/util/text-tracks.test.js b/test/util/text-tracks.test.js index 60ae1be51..8951e8ab2 100644 --- a/test/util/text-tracks.test.js +++ b/test/util/text-tracks.test.js @@ -123,3 +123,38 @@ test('creates cues for timed metadata', function(assert) { 'added one metadata cues' ); }); + +test('does nothing if frames are undefined', function(assert) { + addMetadata({ + inbandTextTracks: this.inbandTextTracks, + timestampOffset: this.timestampOffset, + videoDuration: 1, + metadataArray: [{ + cueTime: 1 + }] + }); + + assert.strictEqual( + this.inbandTextTracks.metadataTrack_.cues.length, + 0, + 'added no metadata cues' + ); +}); + +test('does nothing if frames.length is 0', function(assert) { + addMetadata({ + inbandTextTracks: this.inbandTextTracks, + timestampOffset: this.timestampOffset, + videoDuration: 1, + metadataArray: [{ + cueTime: 1, + frames: [] + }] + }); + + assert.strictEqual( + this.inbandTextTracks.metadataTrack_.cues.length, + 0, + 'added no metadata cues' + ); +}); diff --git a/test/videojs-http-streaming.test.js b/test/videojs-http-streaming.test.js index 5c06bb993..13cf4e8a5 100644 --- a/test/videojs-http-streaming.test.js +++ b/test/videojs-http-streaming.test.js @@ -53,13 +53,6 @@ import {version as mpdVersion} from 'mpd-parser/package.json'; import {version as m3u8Version} from 'm3u8-parser/package.json'; import {version as aesVersion} from 'aes-decrypter/package.json'; -let testOrSkip = 'test'; - -// some tests just don't work reliably on ie11 or edge -if (videojs.browser.IS_EDGE || videojs.browser.IE_VERSION) { - testOrSkip = 'skip'; -} - const ogVhsHandlerSetupQualityLevels = videojs.VhsHandler.prototype.setupQualityLevels_; // do a shallow copy of the properties of source onto the target object @@ -78,10 +71,9 @@ QUnit.module('VHS', { this.mse = useFakeMediaSource(); this.clock = this.env.clock; this.old = {}; - if (!videojs.browser.IE_VERSION) { - this.old.devicePixelRatio = window.devicePixelRatio; - window.devicePixelRatio = 1; - } + this.old.devicePixelRatio = window.devicePixelRatio; + window.devicePixelRatio = 1; + // store functionality that some tests need to mock this.old.GlobalOptions = merge(videojs.options); @@ -404,37 +396,6 @@ QUnit.test('autoplay seeks to the live point after media source open', function( assert.notEqual(currentTime, 0, 'seeked on autoplay'); }); -QUnit.test( - 'autoplay seeks to the live point after tech fires loadedmetadata in ie11', - function(assert) { - videojs.browser.IE_VERSION = 11; - let currentTime = 0; - - this.player.autoplay(true); - this.player.on('seeking', () => { - currentTime = this.player.currentTime(); - }); - this.player.src({ - src: 'liveStart30sBefore.m3u8', - type: 'application/vnd.apple.mpegurl' - }); - - this.clock.tick(1); - - openMediaSource(this.player, this.clock); - this.player.tech_.trigger('play'); - this.standardXHRResponse(this.requests.shift()); - this.clock.tick(1); - - assert.equal(currentTime, 0, 'have not played yet'); - - this.player.tech_.trigger('loadedmetadata'); - this.clock.tick(1); - - assert.notEqual(currentTime, 0, 'seeked after tech is ready'); - } -); - QUnit.test( 'duration is set when the source opens after the playlist is loaded', function(assert) { @@ -950,7 +911,7 @@ QUnit.module('NetworkInformationApi', hooks => { window.navigator = this.ogNavigator; }); - QUnit[testOrSkip]( + QUnit.test( 'bandwidth returns networkInformation.downlink when useNetworkInformationApi option is enabled', function(assert) { this.resetNavigatorConnection({ @@ -973,7 +934,7 @@ QUnit.module('NetworkInformationApi', hooks => { } ); - QUnit[testOrSkip]( + QUnit.test( 'bandwidth uses player-estimated bandwidth when its value is greater than networkInformation.downLink and both values are >= 10 Mbps', function(assert) { this.resetNavigatorConnection({ @@ -997,7 +958,7 @@ QUnit.module('NetworkInformationApi', hooks => { } ); - QUnit[testOrSkip]( + QUnit.test( 'bandwidth uses network-information-api bandwidth when its value is less than the player bandwidth and 10 Mbps', function(assert) { this.resetNavigatorConnection({ @@ -1021,7 +982,7 @@ QUnit.module('NetworkInformationApi', hooks => { } ); - QUnit[testOrSkip]( + QUnit.test( 'bandwidth uses player-estimated bandwidth when networkInformation is not supported', function(assert) { // Nullify the `connection` property on Navigator @@ -4447,21 +4408,11 @@ QUnit.test('eme waitingforkey event triggers another setup', function(assert) { vhs.playlistController_.sourceUpdater_.trigger('createdsourcebuffers'); - // Since IE11 doesn't initialize media keys early, in this test IE11 will always have - // one less call than in other browsers. - if (videojs.browser.IE_VERSION === 11) { - assert.equal(createKeySessionCalls, 0, 'did not call createKeySessions_ yet'); - } else { - assert.equal(createKeySessionCalls, 1, 'called createKeySessions_ once'); - } + assert.equal(createKeySessionCalls, 1, 'called createKeySessions_ once'); this.player.tech_.trigger({type: 'waitingforkey', status: 'usable'}); - if (videojs.browser.IE_VERSION === 11) { - assert.equal(createKeySessionCalls, 1, 'called createKeySessions_ once'); - } else { - assert.equal(createKeySessionCalls, 2, 'called createKeySessions_ again'); - } + assert.equal(createKeySessionCalls, 2, 'called createKeySessions_ again'); }); QUnit.test('integration: configures eme for DASH on source buffer creation', function(assert) { @@ -4604,13 +4555,8 @@ QUnit.test('integration: updates source updater after eme init', function(assert sourceUpdater.on( 'createdsourcebuffers', () => { - let expected = false; + const expected = false; - // IE initializes eme syncronously directly after source buffer - // creation - if (videojs.browser.IE_VERSION) { - expected = true; - } assert.equal(sourceUpdater.hasInitializedAnyEme(), expected, 'correct eme state'); } ); @@ -4632,7 +4578,7 @@ QUnit.test('integration: updates source updater after eme init', function(assert this.standardXHRResponse(this.requests.shift(), audioSegment()); }); -QUnit[testOrSkip]('player error when key session creation rejects promise', function(assert) { +QUnit.test('player error when key session creation rejects promise', function(assert) { const done = assert.async(); this.player.error = (errorObject) => { @@ -4741,7 +4687,7 @@ QUnit.test( } ); -QUnit[testOrSkip]( +QUnit.test( 'stores bandwidth and throughput in localStorage when global option is true', function(assert) { videojs.options.vhs = { @@ -4770,7 +4716,7 @@ QUnit[testOrSkip]( } ); -QUnit[testOrSkip]( +QUnit.test( 'stores bandwidth and throughput in localStorage when player option is true', function(assert) { this.player.dispose(); @@ -4805,7 +4751,7 @@ QUnit[testOrSkip]( } ); -QUnit[testOrSkip]( +QUnit.test( 'stores bandwidth and throughput in localStorage when source option is true', function(assert) { this.player.dispose(); @@ -4835,7 +4781,7 @@ QUnit[testOrSkip]( } ); -QUnit[testOrSkip]( +QUnit.test( 'source localStorage option takes priority over player option', function(assert) { this.player.dispose(); @@ -4871,7 +4817,7 @@ QUnit[testOrSkip]( } ); -QUnit[testOrSkip]( +QUnit.test( 'does not store bandwidth and throughput in localStorage by default', function(assert) { this.player.dispose(); @@ -4897,7 +4843,7 @@ QUnit[testOrSkip]( } ); -QUnit[testOrSkip]('retrieves bandwidth and throughput from localStorage', function(assert) { +QUnit.test('retrieves bandwidth and throughput from localStorage', function(assert) { window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify({ bandwidth: 33, throughput: 44 @@ -4959,7 +4905,7 @@ QUnit[testOrSkip]('retrieves bandwidth and throughput from localStorage', functi videojs.options.vhs = origVhsOptions; }); -QUnit[testOrSkip]( +QUnit.test( 'does not retrieve bandwidth and throughput from localStorage when stored value is not as expected', function(assert) { // bad value