([#18](https://github.com/videojs/mpd-parser/issues/18)) ([71b8976](https://github.com/videojs/mpd-parser/commit/71b8976))
79 | * Support for inheriting BaseURL and alternate BaseURLs ([#17](https://github.com/videojs/mpd-parser/issues/17)) ([7dad5d5](https://github.com/videojs/mpd-parser/commit/7dad5d5))
80 | * add support for SegmentTemplate padding format string and SegmentTimeline ([#16](https://github.com/videojs/mpd-parser/issues/16)) ([87933f6](https://github.com/videojs/mpd-parser/commit/87933f6))
81 |
82 |
83 | ## [0.2.1](https://github.com/videojs/mpd-parser/compare/v0.2.0...v0.2.1) (2017-12-15)
84 |
85 | ### Bug Fixes
86 |
87 | * access HTMLCollections with IE11 compatibility ([#15](https://github.com/videojs/mpd-parser/issues/15)) ([5612984](https://github.com/videojs/mpd-parser/commit/5612984))
88 |
89 |
90 | # [0.2.0](https://github.com/videojs/mpd-parser/compare/v0.1.1...v0.2.0) (2017-12-12)
91 |
92 | ### Features
93 |
94 | * Support for vtt ([#13](https://github.com/videojs/mpd-parser/issues/13)) ([96fc406](https://github.com/videojs/mpd-parser/commit/96fc406))
95 |
96 | ### Tests
97 |
98 | * add more tests for vtt ([#14](https://github.com/videojs/mpd-parser/issues/14)) ([4068790](https://github.com/videojs/mpd-parser/commit/4068790))
99 |
100 |
101 | ## [0.1.1](https://github.com/videojs/mpd-parser/compare/v0.1.0...v0.1.1) (2017-12-07)
102 |
103 | ### Bug Fixes
104 |
105 | * avoid using Array.prototype.fill for IE support ([#11](https://github.com/videojs/mpd-parser/issues/11)) ([5c444de](https://github.com/videojs/mpd-parser/commit/5c444de))
106 |
107 |
108 | # 0.1.0 (2017-11-29)
109 |
110 | ### Bug Fixes
111 |
112 | * switch off in-manifest caption support ([#8](https://github.com/videojs/mpd-parser/issues/8)) ([15712c6](https://github.com/videojs/mpd-parser/commit/15712c6))
113 |
114 | CHANGELOG
115 | =========
116 |
117 | ## HEAD (Unreleased)
118 | _(none)_
119 |
120 | --------------------
121 |
122 |
--------------------------------------------------------------------------------
/mpd-parser/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # CONTRIBUTING
2 |
3 | We welcome contributions from everyone!
4 |
5 | ## Getting Started
6 |
7 | Make sure you have NodeJS 4.0 or higher and npm installed.
8 |
9 | 1. Fork this repository and clone your fork
10 | 1. Install dependencies: `npm install`
11 | 1. Run a development server: `npm start`
12 |
13 | ### Making Changes
14 |
15 | Refer to the [video.js plugin conventions][conventions] for more detail on best practices and tooling for video.js plugin authorship.
16 |
17 | When you've made your changes, push your commit(s) to your fork and issue a pull request against the original repository.
18 |
19 | ### Running Tests
20 |
21 | Testing is a crucial part of any software project. For all but the most trivial changes (typos, etc) test cases are expected. Tests are run in actual browsers using [Karma][karma].
22 |
23 | - In all available and supported browsers: `npm test`
24 | - In a specific browser: `npm run test:chrome`, `npm run test:firefox`, etc.
25 | - While development server is running (`npm start`), navigate to [`http://localhost:9999/test/`][local]
26 |
27 |
28 | [karma]: http://karma-runner.github.io/
29 | [local]: http://localhost:9999/test/
30 | [conventions]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/conventions.md
31 |
--------------------------------------------------------------------------------
/mpd-parser/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Brightcove, Inc
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/mpd-parser/README.md:
--------------------------------------------------------------------------------
1 | # mpd-parser
2 |
3 | [](https://travis-ci.org/videojs/mpd-parser)
4 | [](https://greenkeeper.io/)
5 | [](http://slack.videojs.com)
6 |
7 | [](https://nodei.co/npm/mpd-parser/)
8 |
9 | mpd parser
10 |
11 | ## Table of Contents
12 |
13 |
14 |
15 |
16 |
17 | - [Installation](#installation)
18 | - [Usage](#usage)
19 | - [Parsed Output](#parsed-output)
20 | - [Including the Parser](#including-the-parser)
21 | - [`
129 |
133 | ```
134 |
135 | ### Browserify
136 |
137 | When using with Browserify, install mpd-parser via npm and `require` the parser as you would any other module.
138 |
139 | ```js
140 | var mpdParser = require('mpd-parser');
141 |
142 | var parsedManifest = mpdParser.parse(manifest, manifestUrl);
143 | ```
144 |
145 | With ES6:
146 | ```js
147 | import { parse } from 'mpd-parser';
148 |
149 | const parsedManifest = parse(manifest, manifestUrl);
150 | ```
151 |
152 | ### RequireJS/AMD
153 |
154 | When using with RequireJS (or another AMD library), get the script in whatever way you prefer and `require` the parser as you normally would:
155 |
156 | ```js
157 | require(['mpd-parser'], function(mpdParser) {
158 | var parsedManifest = mpdParser.parse(manifest, manifestUrl);
159 | });
160 | ```
161 |
162 | ## License
163 |
164 | Apache-2.0. Copyright (c) Brightcove, Inc
165 |
--------------------------------------------------------------------------------
/mpd-parser/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mpd-parser Demo
6 |
7 |
8 | Open dev tools to try it out
9 |
13 |
14 |
21 |
22 |
23 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/mpd-parser/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "_from": "mpd-parser",
3 | "_id": "mpd-parser@0.8.1",
4 | "_inBundle": false,
5 | "_integrity": "sha512-WBTJ1bKk8OLUIxBh6s1ju1e2yz/5CzhPbgi6P3F3kJHKhGy1Z+ElvEnuzEbtC/dnbRcJtMXazE3f93N5LLdp9Q==",
6 | "_location": "/mpd-parser",
7 | "_phantomChildren": {},
8 | "_requested": {
9 | "type": "tag",
10 | "registry": true,
11 | "raw": "mpd-parser",
12 | "name": "mpd-parser",
13 | "escapedName": "mpd-parser",
14 | "rawSpec": "",
15 | "saveSpec": null,
16 | "fetchSpec": "latest"
17 | },
18 | "_requiredBy": [
19 | "#USER",
20 | "/"
21 | ],
22 | "_resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.8.1.tgz",
23 | "_shasum": "db299dbec337999fbbbace989d227c7b03dc8ea7",
24 | "_spec": "mpd-parser",
25 | "_where": "/Users/tealbookcsr/Downloads/mpdparse",
26 | "author": {
27 | "name": "Brightcove, Inc"
28 | },
29 | "browserslist": [
30 | "defaults",
31 | "ie 11"
32 | ],
33 | "bugs": {
34 | "url": "https://github.com/videojs/mpd-parser/issues"
35 | },
36 | "bundleDependencies": false,
37 | "dependencies": {
38 | "global": "^4.3.2",
39 | "url-toolkit": "^2.1.1",
40 | "xmldom": "^0.1.27"
41 | },
42 | "deprecated": false,
43 | "description": "mpd parser",
44 | "devDependencies": {
45 | "conventional-changelog-cli": "^2.0.1",
46 | "conventional-changelog-videojs": "^3.0.0",
47 | "doctoc": "^1.3.1",
48 | "husky": "^1.0.0-rc.13",
49 | "jsdoc": "git+https://github.com/BrandonOCasey/jsdoc.git#feat/plugin-from-cli",
50 | "karma": "^3.0.0",
51 | "lint-staged": "^7.2.2",
52 | "not-prerelease": "^1.0.1",
53 | "npm-merge-driver-install": "^1.0.0",
54 | "npm-run-all": "^4.1.5",
55 | "pkg-ok": "^2.2.0",
56 | "rollup": "^0.66.0",
57 | "rollup-plugin-string": "^2.0.2",
58 | "shelljs": "~0.8.2",
59 | "shx": "^0.3.2",
60 | "sinon": "^6.1.5",
61 | "videojs-generate-karma-config": "~5.0.1",
62 | "videojs-generate-rollup-config": "~2.3.0",
63 | "videojs-generator-verify": "~1.0.4",
64 | "videojs-standard": "~7.1.0"
65 | },
66 | "files": [
67 | "CONTRIBUTING.md",
68 | "dist/",
69 | "docs/",
70 | "index.html",
71 | "scripts/",
72 | "src/",
73 | "test/"
74 | ],
75 | "generator-videojs-plugin": {
76 | "version": "7.3.2"
77 | },
78 | "homepage": "https://github.com/videojs/mpd-parser#readme",
79 | "husky": {
80 | "hooks": {
81 | "pre-commit": "lint-staged"
82 | }
83 | },
84 | "keywords": [
85 | "videojs",
86 | "videojs-plugin"
87 | ],
88 | "license": "Apache-2.0",
89 | "lint-staged": {
90 | "*.js": [
91 | "vjsstandard --fix",
92 | "git add"
93 | ],
94 | "README.md": [
95 | "npm run docs:toc",
96 | "git add"
97 | ]
98 | },
99 | "main": "dist/mpd-parser.cjs.js",
100 | "module": "dist/mpd-parser.es.js",
101 | "name": "mpd-parser",
102 | "repository": {
103 | "type": "git",
104 | "url": "git+ssh://git@github.com/videojs/mpd-parser.git"
105 | },
106 | "scripts": {
107 | "build": "npm-run-all -p build:*",
108 | "build:js": "rollup -c scripts/rollup.config.js",
109 | "clean": "shx rm -rf ./dist ./test/dist",
110 | "docs": "npm-run-all docs:*",
111 | "docs:api": "jsdoc src -g plugins/markdown -r -d docs/api",
112 | "docs:toc": "doctoc README.md",
113 | "lint": "vjsstandard",
114 | "netlify": "node scripts/netlify.js",
115 | "postclean": "shx mkdir -p ./dist ./test/dist",
116 | "posttest": "shx cat test/dist/coverage/text.txt",
117 | "prebuild": "npm run clean",
118 | "prenetlify": "npm run build",
119 | "prepublishOnly": "npm run build && vjsverify",
120 | "pretest": "npm-run-all lint build",
121 | "preversion": "npm test",
122 | "server": "karma start scripts/karma.conf.js --singleRun=false --auto-watch",
123 | "start": "npm-run-all -p server watch",
124 | "test": "karma start scripts/karma.conf.js",
125 | "update-changelog": "conventional-changelog -p videojs -i CHANGELOG.md -s",
126 | "version": "is-prerelease || npm run update-changelog && git add CHANGELOG.md",
127 | "watch": "npm-run-all -p watch:*",
128 | "watch:js": "npm run build:js -- -w"
129 | },
130 | "version": "0.8.1",
131 | "vjsstandard": {
132 | "ignore": [
133 | "dist",
134 | "docs",
135 | "test/dist"
136 | ]
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/mpd-parser/scripts/karma.conf.js:
--------------------------------------------------------------------------------
1 | const generate = require('videojs-generate-karma-config');
2 |
3 | module.exports = function(config) {
4 |
5 | // see https://github.com/videojs/videojs-generate-karma-config
6 | // for options
7 | const options = {};
8 |
9 | config = generate(config, options);
10 |
11 | // any other custom stuff not supported by options here!
12 | };
13 |
14 |
--------------------------------------------------------------------------------
/mpd-parser/scripts/netlify.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const sh = require('shelljs');
3 |
4 | const files = ['dist', 'index.html'];
5 | const deployDir = 'deploy';
6 |
7 | // cleanup previous deploy
8 | sh.rm('-rf', deployDir);
9 | // make sure the directory exists
10 | sh.mkdir('-p', deployDir);
11 |
12 | // copy over dist, and html files
13 | files
14 | .forEach((file) => sh.cp('-r', file, path.join(deployDir, file)));
15 |
--------------------------------------------------------------------------------
/mpd-parser/scripts/rollup.config.js:
--------------------------------------------------------------------------------
1 | const generate = require('videojs-generate-rollup-config');
2 | const string = require('rollup-plugin-string');
3 |
4 | // see https://github.com/videojs/videojs-generate-rollup-config
5 | // for options
6 | const options = {
7 | input: 'src/index.js',
8 | plugins(defaults) {
9 | defaults.test.unshift('string');
10 |
11 | return defaults;
12 | },
13 | primedPlugins(defaults) {
14 | defaults.string = string({include: ['test/manifests/*.mpd']});
15 |
16 | return defaults;
17 | }
18 | };
19 | const config = generate(options);
20 |
21 | // Add additonal builds/customization here!
22 |
23 | // export the builds to rollup
24 | export default Object.values(config.builds);
25 |
--------------------------------------------------------------------------------
/mpd-parser/src/errors.js:
--------------------------------------------------------------------------------
1 | export default {
2 | INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD',
3 | DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST',
4 | DASH_INVALID_XML: 'DASH_INVALID_XML',
5 | NO_BASE_URL: 'NO_BASE_URL',
6 | MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION',
7 | SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED',
8 | UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME'
9 | };
10 |
--------------------------------------------------------------------------------
/mpd-parser/src/index.js:
--------------------------------------------------------------------------------
1 | import { version } from '../package.json';
2 | import { toM3u8 } from './toM3u8';
3 | import { toPlaylists } from './toPlaylists';
4 | import { inheritAttributes } from './inheritAttributes';
5 | import { stringToMpdXml } from './stringToMpdXml';
6 | import { parseUTCTimingScheme } from './parseUTCTimingScheme';
7 |
8 | export const VERSION = version;
9 |
10 | export const parse = (manifestString, options = {}) =>
11 | toM3u8(toPlaylists(inheritAttributes(stringToMpdXml(manifestString), options)), options.sidxMapping);
12 |
13 | /**
14 | * Parses the manifest for a UTCTiming node, returning the nodes attributes if found
15 | *
16 | * @param {string} manifestString
17 | * XML string of the MPD manifest
18 | * @return {Object|null}
19 | * Attributes of UTCTiming node specified in the manifest. Null if none found
20 | */
21 | export const parseUTCTiming = (manifestString) =>
22 | parseUTCTimingScheme(stringToMpdXml(manifestString));
23 |
--------------------------------------------------------------------------------
/mpd-parser/src/parseAttributes.js:
--------------------------------------------------------------------------------
1 | import { from } from './utils/list';
2 | import { parseDuration, parseDate } from './utils/time';
3 |
4 | // TODO: maybe order these in some way that makes it easy to find specific attributes
5 | export const parsers = {
6 | /**
7 | * Specifies the duration of the entire Media Presentation. Format is a duration string
8 | * as specified in ISO 8601
9 | *
10 | * @param {string} value
11 | * value of attribute as a string
12 | * @return {number}
13 | * The duration in seconds
14 | */
15 | mediaPresentationDuration(value) {
16 | return parseDuration(value);
17 | },
18 |
19 | /**
20 | * Specifies the Segment availability start time for all Segments referred to in this
21 | * MPD. For a dynamic manifest, it specifies the anchor for the earliest availability
22 | * time. Format is a date string as specified in ISO 8601
23 | *
24 | * @param {string} value
25 | * value of attribute as a string
26 | * @return {number}
27 | * The date as seconds from unix epoch
28 | */
29 | availabilityStartTime(value) {
30 | return parseDate(value) / 1000;
31 | },
32 |
33 | /**
34 | * Specifies the smallest period between potential changes to the MPD. Format is a
35 | * duration string as specified in ISO 8601
36 | *
37 | * @param {string} value
38 | * value of attribute as a string
39 | * @return {number}
40 | * The duration in seconds
41 | */
42 | minimumUpdatePeriod(value) {
43 | return parseDuration(value);
44 | },
45 |
46 | /**
47 | * Specifies the duration of the smallest time shifting buffer for any Representation
48 | * in the MPD. Format is a duration string as specified in ISO 8601
49 | *
50 | * @param {string} value
51 | * value of attribute as a string
52 | * @return {number}
53 | * The duration in seconds
54 | */
55 | timeShiftBufferDepth(value) {
56 | return parseDuration(value);
57 | },
58 |
59 | /**
60 | * Specifies the PeriodStart time of the Period relative to the availabilityStarttime.
61 | * Format is a duration string as specified in ISO 8601
62 | *
63 | * @param {string} value
64 | * value of attribute as a string
65 | * @return {number}
66 | * The duration in seconds
67 | */
68 | start(value) {
69 | return parseDuration(value);
70 | },
71 |
72 | /**
73 | * Specifies the width of the visual presentation
74 | *
75 | * @param {string} value
76 | * value of attribute as a string
77 | * @return {number}
78 | * The parsed width
79 | */
80 | width(value) {
81 | return parseInt(value, 10);
82 | },
83 |
84 | /**
85 | * Specifies the height of the visual presentation
86 | *
87 | * @param {string} value
88 | * value of attribute as a string
89 | * @return {number}
90 | * The parsed height
91 | */
92 | height(value) {
93 | return parseInt(value, 10);
94 | },
95 |
96 | /**
97 | * Specifies the bitrate of the representation
98 | *
99 | * @param {string} value
100 | * value of attribute as a string
101 | * @return {number}
102 | * The parsed bandwidth
103 | */
104 | bandwidth(value) {
105 | return parseInt(value, 10);
106 | },
107 |
108 | /**
109 | * Specifies the number of the first Media Segment in this Representation in the Period
110 | *
111 | * @param {string} value
112 | * value of attribute as a string
113 | * @return {number}
114 | * The parsed number
115 | */
116 | startNumber(value) {
117 | return parseInt(value, 10);
118 | },
119 |
120 | /**
121 | * Specifies the timescale in units per seconds
122 | *
123 | * @param {string} value
124 | * value of attribute as a string
125 | * @return {number}
126 | * The aprsed timescale
127 | */
128 | timescale(value) {
129 | return parseInt(value, 10);
130 | },
131 |
132 | /**
133 | * Specifies the constant approximate Segment duration
134 | * NOTE: The element also contains an @duration attribute. This duration
135 | * specifies the duration of the Period. This attribute is currently not
136 | * supported by the rest of the parser, however we still check for it to prevent
137 | * errors.
138 | *
139 | * @param {string} value
140 | * value of attribute as a string
141 | * @return {number}
142 | * The parsed duration
143 | */
144 | duration(value) {
145 | const parsedValue = parseInt(value, 10);
146 |
147 | if (isNaN(parsedValue)) {
148 | return parseDuration(value);
149 | }
150 |
151 | return parsedValue;
152 | },
153 |
154 | /**
155 | * Specifies the Segment duration, in units of the value of the @timescale.
156 | *
157 | * @param {string} value
158 | * value of attribute as a string
159 | * @return {number}
160 | * The parsed duration
161 | */
162 | d(value) {
163 | return parseInt(value, 10);
164 | },
165 |
166 | /**
167 | * Specifies the MPD start time, in @timescale units, the first Segment in the series
168 | * starts relative to the beginning of the Period
169 | *
170 | * @param {string} value
171 | * value of attribute as a string
172 | * @return {number}
173 | * The parsed time
174 | */
175 | t(value) {
176 | return parseInt(value, 10);
177 | },
178 |
179 | /**
180 | * Specifies the repeat count of the number of following contiguous Segments with the
181 | * same duration expressed by the value of @d
182 | *
183 | * @param {string} value
184 | * value of attribute as a string
185 | * @return {number}
186 | * The parsed number
187 | */
188 | r(value) {
189 | return parseInt(value, 10);
190 | },
191 |
192 | /**
193 | * Default parser for all other attributes. Acts as a no-op and just returns the value
194 | * as a string
195 | *
196 | * @param {string} value
197 | * value of attribute as a string
198 | * @return {string}
199 | * Unparsed value
200 | */
201 | DEFAULT(value) {
202 | return value;
203 | }
204 | };
205 |
206 | /**
207 | * Gets all the attributes and values of the provided node, parses attributes with known
208 | * types, and returns an object with attribute names mapped to values.
209 | *
210 | * @param {Node} el
211 | * The node to parse attributes from
212 | * @return {Object}
213 | * Object with all attributes of el parsed
214 | */
215 | export const parseAttributes = (el) => {
216 | if (!(el && el.attributes)) {
217 | return {};
218 | }
219 |
220 | return from(el.attributes)
221 | .reduce((a, e) => {
222 | const parseFn = parsers[e.name] || parsers.DEFAULT;
223 |
224 | a[e.name] = parseFn(e.value);
225 |
226 | return a;
227 | }, {});
228 | };
229 |
--------------------------------------------------------------------------------
/mpd-parser/src/parseUTCTimingScheme.js:
--------------------------------------------------------------------------------
1 | import { findChildren } from './utils/xml';
2 | import { parseAttributes } from './parseAttributes';
3 | import errors from './errors';
4 |
5 | /**
6 | * Parses the manifest for a UTCTiming node, returning the nodes attributes if found
7 | *
8 | * @param {string} mpd
9 | * XML string of the MPD manifest
10 | * @return {Object|null}
11 | * Attributes of UTCTiming node specified in the manifest. Null if none found
12 | */
13 | export const parseUTCTimingScheme = (mpd) => {
14 | const UTCTimingNode = findChildren(mpd, 'UTCTiming')[0];
15 |
16 | if (!UTCTimingNode) {
17 | return null;
18 | }
19 |
20 | const attributes = parseAttributes(UTCTimingNode);
21 |
22 | switch (attributes.schemeIdUri) {
23 | case 'urn:mpeg:dash:utc:http-head:2014':
24 | case 'urn:mpeg:dash:utc:http-head:2012':
25 | attributes.method = 'HEAD';
26 | break;
27 | case 'urn:mpeg:dash:utc:http-xsdate:2014':
28 | case 'urn:mpeg:dash:utc:http-iso:2014':
29 | case 'urn:mpeg:dash:utc:http-xsdate:2012':
30 | case 'urn:mpeg:dash:utc:http-iso:2012':
31 | attributes.method = 'GET';
32 | break;
33 | case 'urn:mpeg:dash:utc:direct:2014':
34 | case 'urn:mpeg:dash:utc:direct:2012':
35 | attributes.method = 'DIRECT';
36 | attributes.value = Date.parse(attributes.value);
37 | break;
38 | case 'urn:mpeg:dash:utc:http-ntp:2014':
39 | case 'urn:mpeg:dash:utc:ntp:2014':
40 | case 'urn:mpeg:dash:utc:sntp:2014':
41 | default:
42 | throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME);
43 | }
44 |
45 | return attributes;
46 | };
47 |
--------------------------------------------------------------------------------
/mpd-parser/src/segment/durationTimeParser.js:
--------------------------------------------------------------------------------
1 | import { range } from '../utils/list';
2 |
3 | /**
4 | * Functions for calculating the range of available segments in static and dynamic
5 | * manifests.
6 | */
7 | export const segmentRange = {
8 | /**
9 | * Returns the entire range of available segments for a static MPD
10 | *
11 | * @param {Object} attributes
12 | * Inheritied MPD attributes
13 | * @return {{ start: number, end: number }}
14 | * The start and end numbers for available segments
15 | */
16 | static(attributes) {
17 | const {
18 | duration,
19 | timescale = 1,
20 | sourceDuration
21 | } = attributes;
22 |
23 | return {
24 | start: 0,
25 | end: Math.ceil(sourceDuration / (duration / timescale))
26 | };
27 | },
28 |
29 | /**
30 | * Returns the current live window range of available segments for a dynamic MPD
31 | *
32 | * @param {Object} attributes
33 | * Inheritied MPD attributes
34 | * @return {{ start: number, end: number }}
35 | * The start and end numbers for available segments
36 | */
37 | dynamic(attributes) {
38 | const {
39 | NOW,
40 | clientOffset,
41 | availabilityStartTime,
42 | timescale = 1,
43 | duration,
44 | start = 0,
45 | minimumUpdatePeriod = 0,
46 | timeShiftBufferDepth = Infinity
47 | } = attributes;
48 | const now = (NOW + clientOffset) / 1000;
49 | const periodStartWC = availabilityStartTime + start;
50 | const periodEndWC = now + minimumUpdatePeriod;
51 | const periodDuration = periodEndWC - periodStartWC;
52 | const segmentCount = Math.ceil(periodDuration * timescale / duration);
53 | const availableStart =
54 | Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration);
55 | const availableEnd = Math.floor((now - periodStartWC) * timescale / duration);
56 |
57 | return {
58 | start: Math.max(0, availableStart),
59 | end: Math.min(segmentCount, availableEnd)
60 | };
61 | }
62 | };
63 |
64 | /**
65 | * Maps a range of numbers to objects with information needed to build the corresponding
66 | * segment list
67 | *
68 | * @name toSegmentsCallback
69 | * @function
70 | * @param {number} number
71 | * Number of the segment
72 | * @param {number} index
73 | * Index of the number in the range list
74 | * @return {{ number: Number, duration: Number, timeline: Number, time: Number }}
75 | * Object with segment timing and duration info
76 | */
77 |
78 | /**
79 | * Returns a callback for Array.prototype.map for mapping a range of numbers to
80 | * information needed to build the segment list.
81 | *
82 | * @param {Object} attributes
83 | * Inherited MPD attributes
84 | * @return {toSegmentsCallback}
85 | * Callback map function
86 | */
87 | export const toSegments = (attributes) => (number, index) => {
88 | const {
89 | duration,
90 | timescale = 1,
91 | periodIndex,
92 | startNumber = 1
93 | } = attributes;
94 |
95 | return {
96 | number: startNumber + number,
97 | duration: duration / timescale,
98 | timeline: periodIndex,
99 | time: index * duration
100 | };
101 | };
102 |
103 | /**
104 | * Returns a list of objects containing segment timing and duration info used for
105 | * building the list of segments. This uses the @duration attribute specified
106 | * in the MPD manifest to derive the range of segments.
107 | *
108 | * @param {Object} attributes
109 | * Inherited MPD attributes
110 | * @return {{number: number, duration: number, time: number, timeline: number}[]}
111 | * List of Objects with segment timing and duration info
112 | */
113 | export const parseByDuration = (attributes) => {
114 | const {
115 | type = 'static',
116 | duration,
117 | timescale = 1,
118 | sourceDuration
119 | } = attributes;
120 |
121 | const { start, end } = segmentRange[type](attributes);
122 | const segments = range(start, end).map(toSegments(attributes));
123 |
124 | if (type === 'static') {
125 | const index = segments.length - 1;
126 |
127 | // final segment may be less than full segment duration
128 | segments[index].duration = sourceDuration - (duration / timescale * index);
129 | }
130 |
131 | return segments;
132 | };
133 |
--------------------------------------------------------------------------------
/mpd-parser/src/segment/segmentBase.js:
--------------------------------------------------------------------------------
1 | import errors from '../errors';
2 | import urlTypeConverter from './urlType';
3 | import { parseByDuration } from './durationTimeParser';
4 |
5 | /**
6 | * Translates SegmentBase into a set of segments.
7 | * (DASH SPEC Section 5.3.9.3.2) contains a set of nodes. Each
8 | * node should be translated into segment.
9 | *
10 | * @param {Object} attributes
11 | * Object containing all inherited attributes from parent elements with attribute
12 | * names as keys
13 | * @return {Object.} list of segments
14 | */
15 | export const segmentsFromBase = (attributes) => {
16 | const {
17 | baseUrl,
18 | initialization = {},
19 | sourceDuration,
20 | timescale = 1,
21 | indexRange = '',
22 | duration
23 | } = attributes;
24 |
25 | // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1)
26 | if (!baseUrl) {
27 | throw new Error(errors.NO_BASE_URL);
28 | }
29 |
30 | const initSegment = urlTypeConverter({
31 | baseUrl,
32 | source: initialization.sourceURL,
33 | range: initialization.range
34 | });
35 |
36 | const segment = urlTypeConverter({ baseUrl, source: baseUrl, indexRange });
37 |
38 | segment.map = initSegment;
39 |
40 | // If there is a duration, use it, otherwise use the given duration of the source
41 | // (since SegmentBase is only for one total segment)
42 | if (duration) {
43 | const segmentTimeInfo = parseByDuration(attributes);
44 |
45 | if (segmentTimeInfo.length) {
46 | segment.duration = segmentTimeInfo[0].duration;
47 | segment.timeline = segmentTimeInfo[0].timeline;
48 | }
49 | } else if (sourceDuration) {
50 | segment.duration = (sourceDuration / timescale);
51 | segment.timeline = 0;
52 | }
53 |
54 | // This is used for mediaSequence
55 | segment.number = 0;
56 |
57 | return [segment];
58 | };
59 |
60 | /**
61 | * Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist
62 | * according to the sidx information given.
63 | *
64 | * playlist.sidx has metadadata about the sidx where-as the sidx param
65 | * is the parsed sidx box itself.
66 | *
67 | * @param {Object} playlist the playlist to update the sidx information for
68 | * @param {Object} sidx the parsed sidx box
69 | * @return {Object} the playlist object with the updated sidx information
70 | */
71 | export const addSegmentsToPlaylist = (playlist, sidx, baseUrl) => {
72 | // Retain init segment information
73 | const initSegment = playlist.sidx.map ? playlist.sidx.map : null;
74 | // Retain source duration from initial master manifest parsing
75 | const sourceDuration = playlist.sidx.duration;
76 | // Retain source timeline
77 | const timeline = playlist.timeline || 0;
78 | const sidxByteRange = playlist.sidx.byterange;
79 | const sidxEnd = sidxByteRange.offset + sidxByteRange.length;
80 | // Retain timescale of the parsed sidx
81 | const timescale = sidx.timescale;
82 | // referenceType 1 refers to other sidx boxes
83 | const mediaReferences = sidx.references.filter(r => r.referenceType !== 1);
84 | const segments = [];
85 |
86 | // firstOffset is the offset from the end of the sidx box
87 | let startIndex = sidxEnd + sidx.firstOffset;
88 |
89 | for (let i = 0; i < mediaReferences.length; i++) {
90 | const reference = sidx.references[i];
91 | // size of the referenced (sub)segment
92 | const size = reference.referencedSize;
93 | // duration of the referenced (sub)segment, in the timescale
94 | // this will be converted to seconds when generating segments
95 | const duration = reference.subsegmentDuration;
96 | // should be an inclusive range
97 | const endIndex = startIndex + size - 1;
98 | const indexRange = `${startIndex}-${endIndex}`;
99 |
100 | const attributes = {
101 | baseUrl,
102 | timescale,
103 | timeline,
104 | // this is used in parseByDuration
105 | periodIndex: timeline,
106 | duration,
107 | sourceDuration,
108 | indexRange
109 | };
110 |
111 | const segment = segmentsFromBase(attributes)[0];
112 |
113 | if (initSegment) {
114 | segment.map = initSegment;
115 | }
116 |
117 | segments.push(segment);
118 | startIndex += size;
119 | }
120 |
121 | playlist.segments = segments;
122 |
123 | return playlist;
124 | };
125 |
--------------------------------------------------------------------------------
/mpd-parser/src/segment/segmentList.js:
--------------------------------------------------------------------------------
1 | import { parseByTimeline } from './timelineTimeParser';
2 | import { parseByDuration } from './durationTimeParser';
3 | import urlTypeConverter from './urlType';
4 | import errors from '../errors';
5 |
6 | /**
7 | * Converts a (of type URLType from the DASH spec 5.3.9.2 Table 14)
8 | * to an object that matches the output of a segment in videojs/mpd-parser
9 | *
10 | * @param {Object} attributes
11 | * Object containing all inherited attributes from parent elements with attribute
12 | * names as keys
13 | * @param {Object} segmentUrl
14 | * node to translate into a segment object
15 | * @return {Object} translated segment object
16 | */
17 | const SegmentURLToSegmentObject = (attributes, segmentUrl) => {
18 | const { baseUrl, initialization = {} } = attributes;
19 |
20 | const initSegment = urlTypeConverter({
21 | baseUrl,
22 | source: initialization.sourceURL,
23 | range: initialization.range
24 | });
25 |
26 | const segment = urlTypeConverter({
27 | baseUrl,
28 | source: segmentUrl.media,
29 | range: segmentUrl.mediaRange
30 | });
31 |
32 | segment.map = initSegment;
33 |
34 | return segment;
35 | };
36 |
37 | /**
38 | * Generates a list of segments using information provided by the SegmentList element
39 | * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of nodes. Each
40 | * node should be translated into segment.
41 | *
42 | * @param {Object} attributes
43 | * Object containing all inherited attributes from parent elements with attribute
44 | * names as keys
45 | * @param {Object[]|undefined} segmentTimeline
46 | * List of objects representing the attributes of each S element contained within
47 | * the SegmentTimeline element
48 | * @return {Object.} list of segments
49 | */
50 | export const segmentsFromList = (attributes, segmentTimeline) => {
51 | const {
52 | duration,
53 | segmentUrls = []
54 | } = attributes;
55 |
56 | // Per spec (5.3.9.2.1) no way to determine segment duration OR
57 | // if both SegmentTimeline and @duration are defined, it is outside of spec.
58 | if ((!duration && !segmentTimeline) ||
59 | (duration && segmentTimeline)) {
60 | throw new Error(errors.SEGMENT_TIME_UNSPECIFIED);
61 | }
62 |
63 | const segmentUrlMap = segmentUrls.map(segmentUrlObject =>
64 | SegmentURLToSegmentObject(attributes, segmentUrlObject));
65 | let segmentTimeInfo;
66 |
67 | if (duration) {
68 | segmentTimeInfo = parseByDuration(attributes);
69 | }
70 |
71 | if (segmentTimeline) {
72 | segmentTimeInfo = parseByTimeline(attributes, segmentTimeline);
73 | }
74 |
75 | const segments = segmentTimeInfo.map((segmentTime, index) => {
76 | if (segmentUrlMap[index]) {
77 | const segment = segmentUrlMap[index];
78 |
79 | segment.timeline = segmentTime.timeline;
80 | segment.duration = segmentTime.duration;
81 | segment.number = segmentTime.number;
82 | return segment;
83 | }
84 | // Since we're mapping we should get rid of any blank segments (in case
85 | // the given SegmentTimeline is handling for more elements than we have
86 | // SegmentURLs for).
87 | }).filter(segment => segment);
88 |
89 | return segments;
90 | };
91 |
--------------------------------------------------------------------------------
/mpd-parser/src/segment/segmentTemplate.js:
--------------------------------------------------------------------------------
1 | import resolveUrl from '../utils/resolveUrl';
2 | import urlTypeToSegment from './urlType';
3 | import { parseByTimeline } from './timelineTimeParser';
4 | import { parseByDuration } from './durationTimeParser';
5 |
6 | const identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g;
7 |
8 | /**
9 | * Replaces template identifiers with corresponding values. To be used as the callback
10 | * for String.prototype.replace
11 | *
12 | * @name replaceCallback
13 | * @function
14 | * @param {string} match
15 | * Entire match of identifier
16 | * @param {string} identifier
17 | * Name of matched identifier
18 | * @param {string} format
19 | * Format tag string. Its presence indicates that padding is expected
20 | * @param {string} width
21 | * Desired length of the replaced value. Values less than this width shall be left
22 | * zero padded
23 | * @return {string}
24 | * Replacement for the matched identifier
25 | */
26 |
27 | /**
28 | * Returns a function to be used as a callback for String.prototype.replace to replace
29 | * template identifiers
30 | *
31 | * @param {Obect} values
32 | * Object containing values that shall be used to replace known identifiers
33 | * @param {number} values.RepresentationID
34 | * Value of the Representation@id attribute
35 | * @param {number} values.Number
36 | * Number of the corresponding segment
37 | * @param {number} values.Bandwidth
38 | * Value of the Representation@bandwidth attribute.
39 | * @param {number} values.Time
40 | * Timestamp value of the corresponding segment
41 | * @return {replaceCallback}
42 | * Callback to be used with String.prototype.replace to replace identifiers
43 | */
44 | export const identifierReplacement = (values) => (match, identifier, format, width) => {
45 | if (match === '$$') {
46 | // escape sequence
47 | return '$';
48 | }
49 |
50 | if (typeof values[identifier] === 'undefined') {
51 | return match;
52 | }
53 |
54 | const value = '' + values[identifier];
55 |
56 | if (identifier === 'RepresentationID') {
57 | // Format tag shall not be present with RepresentationID
58 | return value;
59 | }
60 |
61 | if (!format) {
62 | width = 1;
63 | } else {
64 | width = parseInt(width, 10);
65 | }
66 |
67 | if (value.length >= width) {
68 | return value;
69 | }
70 |
71 | return `${(new Array(width - value.length + 1)).join('0')}${value}`;
72 | };
73 |
74 | /**
75 | * Constructs a segment url from a template string
76 | *
77 | * @param {string} url
78 | * Template string to construct url from
79 | * @param {Obect} values
80 | * Object containing values that shall be used to replace known identifiers
81 | * @param {number} values.RepresentationID
82 | * Value of the Representation@id attribute
83 | * @param {number} values.Number
84 | * Number of the corresponding segment
85 | * @param {number} values.Bandwidth
86 | * Value of the Representation@bandwidth attribute.
87 | * @param {number} values.Time
88 | * Timestamp value of the corresponding segment
89 | * @return {string}
90 | * Segment url with identifiers replaced
91 | */
92 | export const constructTemplateUrl = (url, values) =>
93 | url.replace(identifierPattern, identifierReplacement(values));
94 |
95 | /**
96 | * Generates a list of objects containing timing and duration information about each
97 | * segment needed to generate segment uris and the complete segment object
98 | *
99 | * @param {Object} attributes
100 | * Object containing all inherited attributes from parent elements with attribute
101 | * names as keys
102 | * @param {Object[]|undefined} segmentTimeline
103 | * List of objects representing the attributes of each S element contained within
104 | * the SegmentTimeline element
105 | * @return {{number: number, duration: number, time: number, timeline: number}[]}
106 | * List of Objects with segment timing and duration info
107 | */
108 | export const parseTemplateInfo = (attributes, segmentTimeline) => {
109 | if (!attributes.duration && !segmentTimeline) {
110 | // if neither @duration or SegmentTimeline are present, then there shall be exactly
111 | // one media segment
112 | return [{
113 | number: attributes.startNumber || 1,
114 | duration: attributes.sourceDuration,
115 | time: 0,
116 | timeline: attributes.periodIndex
117 | }];
118 | }
119 |
120 | if (attributes.duration) {
121 | return parseByDuration(attributes);
122 | }
123 |
124 | return parseByTimeline(attributes, segmentTimeline);
125 | };
126 |
127 | /**
128 | * Generates a list of segments using information provided by the SegmentTemplate element
129 | *
130 | * @param {Object} attributes
131 | * Object containing all inherited attributes from parent elements with attribute
132 | * names as keys
133 | * @param {Object[]|undefined} segmentTimeline
134 | * List of objects representing the attributes of each S element contained within
135 | * the SegmentTimeline element
136 | * @return {Object[]}
137 | * List of segment objects
138 | */
139 | export const segmentsFromTemplate = (attributes, segmentTimeline) => {
140 | const templateValues = {
141 | RepresentationID: attributes.id,
142 | Bandwidth: attributes.bandwidth || 0
143 | };
144 |
145 | const { initialization = { sourceURL: '', range: '' } } = attributes;
146 |
147 | const mapSegment = urlTypeToSegment({
148 | baseUrl: attributes.baseUrl,
149 | source: constructTemplateUrl(initialization.sourceURL, templateValues),
150 | range: initialization.range
151 | });
152 |
153 | const segments = parseTemplateInfo(attributes, segmentTimeline);
154 |
155 | return segments.map(segment => {
156 | templateValues.Number = segment.number;
157 | templateValues.Time = segment.time;
158 |
159 | const uri = constructTemplateUrl(attributes.media || '', templateValues);
160 |
161 | return {
162 | uri,
163 | timeline: segment.timeline,
164 | duration: segment.duration,
165 | resolvedUri: resolveUrl(attributes.baseUrl || '', uri),
166 | map: mapSegment,
167 | number: segment.number
168 | };
169 | });
170 | };
171 |
--------------------------------------------------------------------------------
/mpd-parser/src/segment/timelineTimeParser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Calculates the R (repetition) value for a live stream (for the final segment
3 | * in a manifest where the r value is negative 1)
4 | *
5 | * @param {Object} attributes
6 | * Object containing all inherited attributes from parent elements with attribute
7 | * names as keys
8 | * @param {number} time
9 | * current time (typically the total time up until the final segment)
10 | * @param {number} duration
11 | * duration property for the given
12 | *
13 | * @return {number}
14 | * R value to reach the end of the given period
15 | */
16 | const getLiveRValue = (attributes, time, duration) => {
17 | const {
18 | NOW,
19 | clientOffset,
20 | availabilityStartTime,
21 | timescale = 1,
22 | start = 0,
23 | minimumUpdatePeriod = 0
24 | } = attributes;
25 | const now = (NOW + clientOffset) / 1000;
26 | const periodStartWC = availabilityStartTime + start;
27 | const periodEndWC = now + minimumUpdatePeriod;
28 | const periodDuration = periodEndWC - periodStartWC;
29 |
30 | return Math.ceil(((periodDuration * timescale) - time) / duration);
31 | };
32 |
33 | /**
34 | * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment
35 | * timing and duration
36 | *
37 | * @param {Object} attributes
38 | * Object containing all inherited attributes from parent elements with attribute
39 | * names as keys
40 | * @param {Object[]} segmentTimeline
41 | * List of objects representing the attributes of each S element contained within
42 | *
43 | * @return {{number: number, duration: number, time: number, timeline: number}[]}
44 | * List of Objects with segment timing and duration info
45 | */
46 | export const parseByTimeline = (attributes, segmentTimeline) => {
47 | const {
48 | type = 'static',
49 | minimumUpdatePeriod = 0,
50 | media = '',
51 | sourceDuration,
52 | timescale = 1,
53 | startNumber = 1,
54 | periodIndex: timeline
55 | } = attributes;
56 | const segments = [];
57 | let time = -1;
58 |
59 | for (let sIndex = 0; sIndex < segmentTimeline.length; sIndex++) {
60 | const S = segmentTimeline[sIndex];
61 | const duration = S.d;
62 | const repeat = S.r || 0;
63 | const segmentTime = S.t || 0;
64 |
65 | if (time < 0) {
66 | // first segment
67 | time = segmentTime;
68 | }
69 |
70 | if (segmentTime && segmentTime > time) {
71 | // discontinuity
72 |
73 | // TODO: How to handle this type of discontinuity
74 | // timeline++ here would treat it like HLS discontuity and content would
75 | // get appended without gap
76 | // E.G.
77 | //
78 | //
79 | //
80 | //
81 | // would have $Time$ values of [0, 1, 2, 5]
82 | // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY)
83 | // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP)
84 | // does the value of sourceDuration consider this when calculating arbitrary
85 | // negative @r repeat value?
86 | // E.G. Same elements as above with this added at the end
87 | //
88 | // with a sourceDuration of 10
89 | // Would the 2 gaps be included in the time duration calculations resulting in
90 | // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments
91 | // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ?
92 |
93 | time = segmentTime;
94 | }
95 |
96 | let count;
97 |
98 | if (repeat < 0) {
99 | const nextS = sIndex + 1;
100 |
101 | if (nextS === segmentTimeline.length) {
102 | // last segment
103 | if (type === 'dynamic' &&
104 | minimumUpdatePeriod > 0 &&
105 | media.indexOf('$Number$') > 0) {
106 | count = getLiveRValue(attributes, time, duration);
107 | } else {
108 | // TODO: This may be incorrect depending on conclusion of TODO above
109 | count = ((sourceDuration * timescale) - time) / duration;
110 | }
111 | } else {
112 | count = (segmentTimeline[nextS].t - time) / duration;
113 | }
114 | } else {
115 | count = repeat + 1;
116 | }
117 |
118 | const end = startNumber + segments.length + count;
119 | let number = startNumber + segments.length;
120 |
121 | while (number < end) {
122 | segments.push({ number, duration: duration / timescale, time, timeline });
123 | time += duration;
124 | number++;
125 | }
126 | }
127 |
128 | return segments;
129 | };
130 |
--------------------------------------------------------------------------------
/mpd-parser/src/segment/urlType.js:
--------------------------------------------------------------------------------
1 | import resolveUrl from '../utils/resolveUrl';
2 |
3 | /**
4 | * @typedef {Object} SingleUri
5 | * @property {string} uri - relative location of segment
6 | * @property {string} resolvedUri - resolved location of segment
7 | * @property {Object} byterange - Object containing information on how to make byte range
8 | * requests following byte-range-spec per RFC2616.
9 | * @property {String} byterange.length - length of range request
10 | * @property {String} byterange.offset - byte offset of range request
11 | *
12 | * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
13 | */
14 |
15 | /**
16 | * Converts a URLType node (5.3.9.2.3 Table 13) to a segment object
17 | * that conforms to how m3u8-parser is structured
18 | *
19 | * @see https://github.com/videojs/m3u8-parser
20 | *
21 | * @param {string} baseUrl - baseUrl provided by nodes
22 | * @param {string} source - source url for segment
23 | * @param {string} range - optional range used for range calls,
24 | * follows RFC 2616, Clause 14.35.1
25 | * @return {SingleUri} full segment information transformed into a format similar
26 | * to m3u8-parser
27 | */
28 | export const urlTypeToSegment = ({ baseUrl = '', source = '', range = '', indexRange = '' }) => {
29 | const segment = {
30 | uri: source,
31 | resolvedUri: resolveUrl(baseUrl || '', source)
32 | };
33 |
34 | if (range || indexRange) {
35 | const rangeStr = range ? range : indexRange;
36 | const ranges = rangeStr.split('-');
37 | const startRange = parseInt(ranges[0], 10);
38 | const endRange = parseInt(ranges[1], 10);
39 |
40 | // byterange should be inclusive according to
41 | // RFC 2616, Clause 14.35.1
42 | segment.byterange = {
43 | length: endRange - startRange + 1,
44 | offset: startRange
45 | };
46 | }
47 |
48 | return segment;
49 | };
50 |
51 | export const byteRangeToString = (byterange) => {
52 | // `endRange` is one less than `offset + length` because the HTTP range
53 | // header uses inclusive ranges
54 | const endRange = byterange.offset + byterange.length - 1;
55 |
56 | return `${byterange.offset}-${endRange}`;
57 | };
58 |
59 | export default urlTypeToSegment;
60 |
--------------------------------------------------------------------------------
/mpd-parser/src/stringToMpdXml.js:
--------------------------------------------------------------------------------
1 | import window from 'global/window';
2 | import errors from './errors';
3 |
4 | export const stringToMpdXml = (manifestString) => {
5 | if (manifestString === '') {
6 | throw new Error(errors.DASH_EMPTY_MANIFEST);
7 | }
8 |
9 | const parser = new window.DOMParser();
10 | const xml = parser.parseFromString(manifestString, 'application/xml');
11 | const mpd = xml && xml.documentElement.tagName === 'MPD' ?
12 | xml.documentElement : null;
13 |
14 | if (!mpd || mpd &&
15 | mpd.getElementsByTagName('parsererror').length > 0) {
16 | throw new Error(errors.DASH_INVALID_XML);
17 | }
18 |
19 | return mpd;
20 | };
21 |
--------------------------------------------------------------------------------
/mpd-parser/src/toM3u8.js:
--------------------------------------------------------------------------------
1 | import { values } from './utils/object';
2 | import { findIndexes } from './utils/list';
3 | import { addSegmentsToPlaylist } from './segment/segmentBase';
4 | import { byteRangeToString } from './segment/urlType';
5 |
6 | const mergeDiscontiguousPlaylists = playlists => {
7 | const mergedPlaylists = values(playlists.reduce((acc, playlist) => {
8 | // assuming playlist IDs are the same across periods
9 | // TODO: handle multiperiod where representation sets are not the same
10 | // across periods
11 | const name = playlist.attributes.id + (playlist.attributes.lang || '');
12 |
13 | // Periods after first
14 | if (acc[name]) {
15 | // first segment of subsequent periods signal a discontinuity
16 | if (playlist.segments[0]) {
17 | playlist.segments[0].discontinuity = true;
18 | }
19 | acc[name].segments.push(...playlist.segments);
20 |
21 | // bubble up contentProtection, this assumes all DRM content
22 | // has the same contentProtection
23 | if (playlist.attributes.contentProtection) {
24 | acc[name].attributes.contentProtection =
25 | playlist.attributes.contentProtection;
26 | }
27 | } else {
28 | // first Period
29 | acc[name] = playlist;
30 | }
31 |
32 | return acc;
33 | }, {}));
34 |
35 | return mergedPlaylists.map(playlist => {
36 | playlist.discontinuityStarts =
37 | findIndexes(playlist.segments, 'discontinuity');
38 |
39 | return playlist;
40 | });
41 | };
42 |
43 | const addSegmentInfoFromSidx = (playlists, sidxMapping = {}) => {
44 | if (!Object.keys(sidxMapping).length) {
45 | return playlists;
46 | }
47 |
48 | for (const i in playlists) {
49 | const playlist = playlists[i];
50 |
51 | if (!playlist.sidx) {
52 | continue;
53 | }
54 |
55 | const sidxKey = playlist.sidx.uri + '-' +
56 | byteRangeToString(playlist.sidx.byterange);
57 | const sidxMatch = sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx;
58 |
59 | if (playlist.sidx && sidxMatch) {
60 | addSegmentsToPlaylist(playlist, sidxMatch, playlist.sidx.resolvedUri);
61 | }
62 | }
63 |
64 | return playlists;
65 | };
66 |
67 | export const formatAudioPlaylist = ({ attributes, segments, sidx }) => {
68 | const playlist = {
69 | attributes: {
70 | NAME: attributes.id,
71 | BANDWIDTH: attributes.bandwidth,
72 | CODECS: attributes.codecs,
73 | ['PROGRAM-ID']: 1
74 | },
75 | uri: '',
76 | endList: (attributes.type || 'static') === 'static',
77 | timeline: attributes.periodIndex,
78 | resolvedUri: '',
79 | targetDuration: attributes.duration,
80 | segments,
81 | mediaSequence: segments.length ? segments[0].number : 1
82 | };
83 |
84 | if (attributes.contentProtection) {
85 | playlist.contentProtection = attributes.contentProtection;
86 | }
87 |
88 | if (sidx) {
89 | playlist.sidx = sidx;
90 | }
91 |
92 | return playlist;
93 | };
94 |
95 | export const formatVttPlaylist = ({ attributes, segments }) => {
96 | if (typeof segments === 'undefined') {
97 | // vtt tracks may use single file in BaseURL
98 | segments = [{
99 | uri: attributes.baseUrl,
100 | timeline: attributes.periodIndex,
101 | resolvedUri: attributes.baseUrl || '',
102 | duration: attributes.sourceDuration,
103 | number: 0
104 | }];
105 | // targetDuration should be the same duration as the only segment
106 | attributes.duration = attributes.sourceDuration;
107 | }
108 | return {
109 | attributes: {
110 | NAME: attributes.id,
111 | BANDWIDTH: attributes.bandwidth,
112 | ['PROGRAM-ID']: 1
113 | },
114 | uri: '',
115 | endList: (attributes.type || 'static') === 'static',
116 | timeline: attributes.periodIndex,
117 | resolvedUri: attributes.baseUrl || '',
118 | targetDuration: attributes.duration,
119 | segments,
120 | mediaSequence: segments.length ? segments[0].number : 1
121 | };
122 | };
123 |
124 | export const organizeAudioPlaylists = (playlists, sidxMapping = {}) => {
125 | let mainPlaylist;
126 |
127 | const formattedPlaylists = playlists.reduce((a, playlist) => {
128 | const role = playlist.attributes.role &&
129 | playlist.attributes.role.value || '';
130 | const language = playlist.attributes.lang || '';
131 |
132 | let label = 'main';
133 |
134 | if (language) {
135 | const roleLabel = role ? ` (${role})` : '';
136 |
137 | label = `${playlist.attributes.lang}${roleLabel}`;
138 | }
139 |
140 | // skip if we already have the highest quality audio for a language
141 | if (a[label] &&
142 | a[label].playlists[0].attributes.BANDWIDTH >
143 | playlist.attributes.bandwidth) {
144 | return a;
145 | }
146 |
147 | a[label] = {
148 | language,
149 | autoselect: true,
150 | default: role === 'main',
151 | playlists: addSegmentInfoFromSidx(
152 | [formatAudioPlaylist(playlist)],
153 | sidxMapping
154 | ),
155 | uri: ''
156 | };
157 |
158 | if (typeof mainPlaylist === 'undefined' && role === 'main') {
159 | mainPlaylist = playlist;
160 | mainPlaylist.default = true;
161 | }
162 |
163 | return a;
164 | }, {});
165 |
166 | // if no playlists have role "main", mark the first as main
167 | if (!mainPlaylist) {
168 | const firstLabel = Object.keys(formattedPlaylists)[0];
169 |
170 | formattedPlaylists[firstLabel].default = true;
171 | }
172 |
173 | return formattedPlaylists;
174 | };
175 |
176 | export const organizeVttPlaylists = (playlists, sidxMapping = {}) => {
177 | return playlists.reduce((a, playlist) => {
178 | const label = playlist.attributes.lang || 'text';
179 |
180 | // skip if we already have subtitles
181 | if (a[label]) {
182 | return a;
183 | }
184 |
185 | a[label] = {
186 | language: label,
187 | default: false,
188 | autoselect: false,
189 | playlists: addSegmentInfoFromSidx(
190 | [formatVttPlaylist(playlist)],
191 | sidxMapping
192 | ),
193 | uri: ''
194 | };
195 |
196 | return a;
197 | }, {});
198 | };
199 |
200 | export const formatVideoPlaylist = ({ attributes, segments, sidx }) => {
201 | const playlist = {
202 | attributes: {
203 | NAME: attributes.id,
204 | AUDIO: 'audio',
205 | SUBTITLES: 'subs',
206 | RESOLUTION: {
207 | width: attributes.width,
208 | height: attributes.height
209 | },
210 | CODECS: attributes.codecs,
211 | BANDWIDTH: attributes.bandwidth,
212 | ['PROGRAM-ID']: 1
213 | },
214 | uri: '',
215 | endList: (attributes.type || 'static') === 'static',
216 | timeline: attributes.periodIndex,
217 | resolvedUri: '',
218 | targetDuration: attributes.duration,
219 | segments,
220 | mediaSequence: segments.length ? segments[0].number : 1
221 | };
222 |
223 | if (attributes.contentProtection) {
224 | playlist.contentProtection = attributes.contentProtection;
225 | }
226 |
227 | if (sidx) {
228 | playlist.sidx = sidx;
229 | }
230 |
231 | return playlist;
232 | };
233 |
234 | export const toM3u8 = (dashPlaylists, sidxMapping = {}) => {
235 | if (!dashPlaylists.length) {
236 | return {};
237 | }
238 |
239 | // grab all master attributes
240 | const {
241 | sourceDuration: duration,
242 | minimumUpdatePeriod = 0
243 | } = dashPlaylists[0].attributes;
244 |
245 | const videoOnly = ({ attributes }) =>
246 | attributes.mimeType === 'video/mp4' || attributes.contentType === 'video';
247 | const audioOnly = ({ attributes }) =>
248 | attributes.mimeType === 'audio/mp4' || attributes.contentType === 'audio';
249 | const vttOnly = ({ attributes }) =>
250 | attributes.mimeType === 'text/vtt' || attributes.contentType === 'text';
251 |
252 | const videoPlaylists = mergeDiscontiguousPlaylists(
253 | dashPlaylists.filter(videoOnly)
254 | ).map(formatVideoPlaylist);
255 | const audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly));
256 | const vttPlaylists = dashPlaylists.filter(vttOnly);
257 |
258 | const master = {
259 | allowCache: true,
260 | discontinuityStarts: [],
261 | segments: [],
262 | endList: true,
263 | mediaGroups: {
264 | AUDIO: {},
265 | VIDEO: {},
266 | ['CLOSED-CAPTIONS']: {},
267 | SUBTITLES: {}
268 | },
269 | uri: '',
270 | duration,
271 | playlists: addSegmentInfoFromSidx(videoPlaylists, sidxMapping),
272 | minimumUpdatePeriod: minimumUpdatePeriod * 1000
273 | };
274 |
275 | if (audioPlaylists.length) {
276 | master.mediaGroups.AUDIO.audio = organizeAudioPlaylists(audioPlaylists, sidxMapping);
277 | }
278 |
279 | if (vttPlaylists.length) {
280 | master.mediaGroups.SUBTITLES.subs = organizeVttPlaylists(vttPlaylists, sidxMapping);
281 | }
282 |
283 | return master;
284 | };
285 |
--------------------------------------------------------------------------------
/mpd-parser/src/toPlaylists.js:
--------------------------------------------------------------------------------
1 | import { merge } from './utils/object';
2 | import { segmentsFromTemplate } from './segment/segmentTemplate';
3 | import { segmentsFromList } from './segment/segmentList';
4 | import { segmentsFromBase } from './segment/segmentBase';
5 |
6 | export const generateSegments = ({ attributes, segmentInfo }) => {
7 | let segmentAttributes;
8 | let segmentsFn;
9 |
10 | if (segmentInfo.template) {
11 | segmentsFn = segmentsFromTemplate;
12 | segmentAttributes = merge(attributes, segmentInfo.template);
13 | } else if (segmentInfo.base) {
14 | segmentsFn = segmentsFromBase;
15 | segmentAttributes = merge(attributes, segmentInfo.base);
16 | } else if (segmentInfo.list) {
17 | segmentsFn = segmentsFromList;
18 | segmentAttributes = merge(attributes, segmentInfo.list);
19 | }
20 |
21 | const segmentsInfo = {
22 | attributes
23 | };
24 |
25 | if (!segmentsFn) {
26 | return segmentsInfo;
27 | }
28 |
29 | const segments = segmentsFn(segmentAttributes, segmentInfo.timeline);
30 |
31 | // The @duration attribute will be used to determin the playlist's targetDuration which
32 | // must be in seconds. Since we've generated the segment list, we no longer need
33 | // @duration to be in @timescale units, so we can convert it here.
34 | if (segmentAttributes.duration) {
35 | const { duration, timescale = 1 } = segmentAttributes;
36 |
37 | segmentAttributes.duration = duration / timescale;
38 | } else if (segments.length) {
39 | // if there is no @duration attribute, use the largest segment duration as
40 | // as target duration
41 | segmentAttributes.duration = segments.reduce((max, segment) => {
42 | return Math.max(max, Math.ceil(segment.duration));
43 | }, 0);
44 | } else {
45 | segmentAttributes.duration = 0;
46 | }
47 |
48 | segmentsInfo.attributes = segmentAttributes;
49 | segmentsInfo.segments = segments;
50 |
51 | // This is a sidx box without actual segment information
52 | if (segmentInfo.base && segmentAttributes.indexRange) {
53 | segmentsInfo.sidx = segments[0];
54 | segmentsInfo.segments = [];
55 | }
56 |
57 | return segmentsInfo;
58 | };
59 |
60 | export const toPlaylists = (representations) => representations.map(generateSegments);
61 |
--------------------------------------------------------------------------------
/mpd-parser/src/utils/decodeB64ToUint8Array.js:
--------------------------------------------------------------------------------
1 | import window from 'global/window';
2 |
3 | export default function decodeB64ToUint8Array(b64Text) {
4 | const decodedString = window.atob(b64Text);
5 | const array = new Uint8Array(decodedString.length);
6 |
7 | for (let i = 0; i < decodedString.length; i++) {
8 | array[i] = decodedString.charCodeAt(i);
9 | }
10 | return array;
11 | }
12 |
--------------------------------------------------------------------------------
/mpd-parser/src/utils/list.js:
--------------------------------------------------------------------------------
1 | export const range = (start, end) => {
2 | const result = [];
3 |
4 | for (let i = start; i < end; i++) {
5 | result.push(i);
6 | }
7 |
8 | return result;
9 | };
10 |
11 | export const flatten = lists => lists.reduce((x, y) => x.concat(y), []);
12 |
13 | export const from = list => {
14 | if (!list.length) {
15 | return [];
16 | }
17 |
18 | const result = [];
19 |
20 | for (let i = 0; i < list.length; i++) {
21 | result.push(list[i]);
22 | }
23 |
24 | return result;
25 | };
26 |
27 | export const findIndexes = (l, key) => l.reduce((a, e, i) => {
28 | if (e[key]) {
29 | a.push(i);
30 | }
31 |
32 | return a;
33 | }, []);
34 |
--------------------------------------------------------------------------------
/mpd-parser/src/utils/object.js:
--------------------------------------------------------------------------------
1 | const isObject = (obj) => {
2 | return !!obj && typeof obj === 'object';
3 | };
4 |
5 | export const merge = (...objects) => {
6 |
7 | return objects.reduce((result, source) => {
8 |
9 | Object.keys(source).forEach(key => {
10 |
11 | if (Array.isArray(result[key]) && Array.isArray(source[key])) {
12 | result[key] = result[key].concat(source[key]);
13 | } else if (isObject(result[key]) && isObject(source[key])) {
14 | result[key] = merge(result[key], source[key]);
15 | } else {
16 | result[key] = source[key];
17 | }
18 | });
19 | return result;
20 | }, {});
21 | };
22 |
23 | export const values = o => Object.keys(o).map(k => o[k]);
24 |
--------------------------------------------------------------------------------
/mpd-parser/src/utils/resolveUrl.js:
--------------------------------------------------------------------------------
1 | import URLToolkit from 'url-toolkit';
2 | import window from 'global/window';
3 |
4 | const resolveUrl = (baseUrl, relativeUrl) => {
5 | // return early if we don't need to resolve
6 | if ((/^[a-z]+:/i).test(relativeUrl)) {
7 | return relativeUrl;
8 | }
9 |
10 | // if the base URL is relative then combine with the current location
11 | if (!(/\/\//i).test(baseUrl)) {
12 | baseUrl = URLToolkit.buildAbsoluteURL(window.location.href, baseUrl);
13 | }
14 |
15 | return URLToolkit.buildAbsoluteURL(baseUrl, relativeUrl);
16 | };
17 |
18 | export default resolveUrl;
19 |
--------------------------------------------------------------------------------
/mpd-parser/src/utils/time.js:
--------------------------------------------------------------------------------
1 | export const parseDuration = (str) => {
2 | const SECONDS_IN_YEAR = 365 * 24 * 60 * 60;
3 | const SECONDS_IN_MONTH = 30 * 24 * 60 * 60;
4 | const SECONDS_IN_DAY = 24 * 60 * 60;
5 | const SECONDS_IN_HOUR = 60 * 60;
6 | const SECONDS_IN_MIN = 60;
7 |
8 | // P10Y10M10DT10H10M10.1S
9 | const durationRegex =
10 | /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/;
11 | const match = durationRegex.exec(str);
12 |
13 | if (!match) {
14 | return 0;
15 | }
16 |
17 | const [year, month, day, hour, minute, second] = match.slice(1);
18 |
19 | return (parseFloat(year || 0) * SECONDS_IN_YEAR +
20 | parseFloat(month || 0) * SECONDS_IN_MONTH +
21 | parseFloat(day || 0) * SECONDS_IN_DAY +
22 | parseFloat(hour || 0) * SECONDS_IN_HOUR +
23 | parseFloat(minute || 0) * SECONDS_IN_MIN +
24 | parseFloat(second || 0));
25 | };
26 |
27 | export const parseDate = (str) => {
28 | // Date format without timezone according to ISO 8601
29 | // YYY-MM-DDThh:mm:ss.ssssss
30 | const dateRegex = /^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/;
31 |
32 | // If the date string does not specifiy a timezone, we must specifiy UTC. This is
33 | // expressed by ending with 'Z'
34 | if (dateRegex.test(str)) {
35 | str += 'Z';
36 | }
37 |
38 | return Date.parse(str);
39 | };
40 |
--------------------------------------------------------------------------------
/mpd-parser/src/utils/xml.js:
--------------------------------------------------------------------------------
1 | import { from } from './list';
2 |
3 | export const findChildren = (element, name) =>
4 | from(element.childNodes).filter(({tagName}) => tagName === name);
5 |
6 | export const getContent = element => element.textContent.trim();
7 |
--------------------------------------------------------------------------------
/mpd-parser/test/index.test.js:
--------------------------------------------------------------------------------
1 | import { parse, VERSION } from '../src';
2 | import QUnit from 'qunit';
3 |
4 | // manifests
5 | import maatVttSegmentTemplate from './manifests/maat_vtt_segmentTemplate.mpd';
6 | import segmentBaseTemplate from './manifests/segmentBase.mpd';
7 | import segmentListTemplate from './manifests/segmentList.mpd';
8 | import multiperiod from './manifests/multiperiod.mpd';
9 | import {
10 | parsedManifest as maatVttSegmentTemplateManifest
11 | } from './manifests/maat_vtt_segmentTemplate.js';
12 | import {
13 | parsedManifest as segmentBaseManifest
14 | } from './manifests/segmentBase.js';
15 | import {
16 | parsedManifest as segmentListManifest
17 | } from './manifests/segmentList.js';
18 | import {
19 | parsedManifest as multiperiodManifest
20 | } from './manifests/multiperiod.js';
21 |
22 | QUnit.module('mpd-parser');
23 |
24 | QUnit.test('has VERSION', function(assert) {
25 | assert.ok(VERSION);
26 | });
27 |
28 | QUnit.test('has parse', function(assert) {
29 | assert.ok(parse);
30 | });
31 |
32 | [{
33 | name: 'maat_vtt_segmentTemplate',
34 | input: maatVttSegmentTemplate,
35 | expected: maatVttSegmentTemplateManifest
36 | }, {
37 | name: 'segmentBase',
38 | input: segmentBaseTemplate,
39 | expected: segmentBaseManifest
40 | }, {
41 | name: 'segmentList',
42 | input: segmentListTemplate,
43 | expected: segmentListManifest
44 | }, {
45 | name: 'multiperiod',
46 | input: multiperiod,
47 | expected: multiperiodManifest
48 | }].forEach(({ name, input, expected }) => {
49 | QUnit.test(`${name} test manifest`, function(assert) {
50 | const actual = parse(input);
51 |
52 | assert.deepEqual(actual, expected);
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/mpd-parser/test/manifests/maat_vtt_segmentTemplate.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | allowCache: true,
3 | discontinuityStarts: [],
4 | duration: 6,
5 | endList: true,
6 | minimumUpdatePeriod: 0,
7 | mediaGroups: {
8 | AUDIO: {
9 | audio: {
10 | ['en (main)']: {
11 | autoselect: true,
12 | default: true,
13 | language: 'en',
14 | playlists: [{
15 | attributes: {
16 | BANDWIDTH: 125000,
17 | CODECS: 'mp4a.40.2',
18 | NAME: '125000',
19 | ['PROGRAM-ID']: 1
20 | },
21 | contentProtection: {
22 | 'com.widevine.alpha': {
23 | attributes: {
24 | schemeIdUri: 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'
25 | },
26 | pssh: new Uint8Array([181, 235, 45])
27 | }
28 | },
29 | endList: true,
30 | mediaSequence: 0,
31 | targetDuration: 1.984,
32 | resolvedUri: '',
33 | segments: [{
34 | duration: 1.984,
35 | map: {
36 | resolvedUri: 'https://www.example.com/125000/init.m4f',
37 | uri: '125000/init.m4f'
38 | },
39 | resolvedUri: 'https://www.example.com/125000/0.m4f',
40 | timeline: 0,
41 | uri: '125000/0.m4f',
42 | number: 0
43 | }, {
44 | duration: 1.984,
45 | map: {
46 | resolvedUri: 'https://www.example.com/125000/init.m4f',
47 | uri: '125000/init.m4f'
48 | },
49 | resolvedUri: 'https://www.example.com/125000/1.m4f',
50 | timeline: 0,
51 | uri: '125000/1.m4f',
52 | number: 1
53 | }, {
54 | duration: 1.984,
55 | map: {
56 | resolvedUri: 'https://www.example.com/125000/init.m4f',
57 | uri: '125000/init.m4f'
58 | },
59 | resolvedUri: 'https://www.example.com/125000/2.m4f',
60 | timeline: 0,
61 | uri: '125000/2.m4f',
62 | number: 2
63 | }, {
64 | duration: 0.04800000000000004,
65 | map: {
66 | resolvedUri: 'https://www.example.com/125000/init.m4f',
67 | uri: '125000/init.m4f'
68 | },
69 | resolvedUri: 'https://www.example.com/125000/3.m4f',
70 | timeline: 0,
71 | uri: '125000/3.m4f',
72 | number: 3
73 | }],
74 | timeline: 0,
75 | uri: ''
76 | }],
77 | uri: ''
78 | },
79 | ['es']: {
80 | autoselect: true,
81 | default: false,
82 | language: 'es',
83 | playlists: [{
84 | attributes: {
85 | BANDWIDTH: 125000,
86 | CODECS: 'mp4a.40.2',
87 | NAME: '125000',
88 | ['PROGRAM-ID']: 1
89 | },
90 | contentProtection: {
91 | 'com.widevine.alpha': {
92 | attributes: {
93 | schemeIdUri: 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'
94 | },
95 | pssh: new Uint8Array([181, 235, 45])
96 | }
97 | },
98 | endList: true,
99 | targetDuration: 1.984,
100 | mediaSequence: 0,
101 | resolvedUri: '',
102 | segments: [{
103 | duration: 1.984,
104 | map: {
105 | resolvedUri: 'https://www.example.com/125000/es/init.m4f',
106 | uri: '125000/es/init.m4f'
107 | },
108 | resolvedUri: 'https://www.example.com/125000/es/0.m4f',
109 | timeline: 0,
110 | uri: '125000/es/0.m4f',
111 | number: 0
112 | }, {
113 | duration: 1.984,
114 | map: {
115 | resolvedUri: 'https://www.example.com/125000/es/init.m4f',
116 | uri: '125000/es/init.m4f'
117 | },
118 | resolvedUri: 'https://www.example.com/125000/es/1.m4f',
119 | timeline: 0,
120 | uri: '125000/es/1.m4f',
121 | number: 1
122 | }, {
123 | duration: 1.984,
124 | map: {
125 | resolvedUri: 'https://www.example.com/125000/es/init.m4f',
126 | uri: '125000/es/init.m4f'
127 | },
128 | resolvedUri: 'https://www.example.com/125000/es/2.m4f',
129 | timeline: 0,
130 | uri: '125000/es/2.m4f',
131 | number: 2
132 | }, {
133 | duration: 0.04800000000000004,
134 | map: {
135 | resolvedUri: 'https://www.example.com/125000/es/init.m4f',
136 | uri: '125000/es/init.m4f'
137 | },
138 | resolvedUri: 'https://www.example.com/125000/es/3.m4f',
139 | timeline: 0,
140 | uri: '125000/es/3.m4f',
141 | number: 3
142 | }],
143 | timeline: 0,
144 | uri: ''
145 | }],
146 | uri: ''
147 | }
148 | }
149 | },
150 | ['CLOSED-CAPTIONS']: {},
151 | SUBTITLES: {
152 | subs: {
153 | en: {
154 | autoselect: false,
155 | default: false,
156 | language: 'en',
157 | playlists: [{
158 | attributes: {
159 | BANDWIDTH: 256,
160 | NAME: 'en',
161 | ['PROGRAM-ID']: 1
162 | },
163 | mediaSequence: 0,
164 | endList: true,
165 | targetDuration: 6,
166 | resolvedUri: 'https://example.com/en.vtt',
167 | segments: [{
168 | duration: 6,
169 | resolvedUri: 'https://example.com/en.vtt',
170 | timeline: 0,
171 | uri: 'https://example.com/en.vtt',
172 | number: 0
173 | }],
174 | timeline: 0,
175 | uri: ''
176 | }],
177 | uri: ''
178 | },
179 | es: {
180 | autoselect: false,
181 | default: false,
182 | language: 'es',
183 | playlists: [{
184 | attributes: {
185 | BANDWIDTH: 256,
186 | NAME: 'es',
187 | ['PROGRAM-ID']: 1
188 | },
189 | endList: true,
190 | targetDuration: 6,
191 | mediaSequence: 0,
192 | resolvedUri: 'https://example.com/es.vtt',
193 | segments: [{
194 | duration: 6,
195 | resolvedUri: 'https://example.com/es.vtt',
196 | timeline: 0,
197 | uri: 'https://example.com/es.vtt',
198 | number: 0
199 | }],
200 | timeline: 0,
201 | uri: ''
202 | }],
203 | uri: ''
204 | }
205 | }
206 | },
207 | VIDEO: {}
208 | },
209 | playlists: [{
210 | attributes: {
211 | AUDIO: 'audio',
212 | SUBTITLES: 'subs',
213 | BANDWIDTH: 449000,
214 | CODECS: 'avc1.420015',
215 | NAME: '482',
216 | ['PROGRAM-ID']: 1,
217 | RESOLUTION: {
218 | height: 270,
219 | width: 482
220 | }
221 | },
222 | contentProtection: {
223 | 'com.widevine.alpha': {
224 | attributes: {
225 | schemeIdUri: 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'
226 | },
227 | pssh: new Uint8Array([181, 235, 45])
228 | }
229 | },
230 | endList: true,
231 | targetDuration: 1.9185833333333333,
232 | mediaSequence: 0,
233 | resolvedUri: '',
234 | segments: [{
235 | duration: 1.9185833333333333,
236 | map: {
237 | resolvedUri: 'https://www.example.com/482/init.m4f',
238 | uri: '482/init.m4f'
239 | },
240 | resolvedUri: 'https://www.example.com/482/0.m4f',
241 | timeline: 0,
242 | uri: '482/0.m4f',
243 | number: 0
244 | }, {
245 | duration: 1.9185833333333333,
246 | map: {
247 | resolvedUri: 'https://www.example.com/482/init.m4f',
248 | uri: '482/init.m4f'
249 | },
250 | resolvedUri: 'https://www.example.com/482/1.m4f',
251 | timeline: 0,
252 | uri: '482/1.m4f',
253 | number: 1
254 | }, {
255 | duration: 1.9185833333333333,
256 | map: {
257 | resolvedUri: 'https://www.example.com/482/init.m4f',
258 | uri: '482/init.m4f'
259 | },
260 | resolvedUri: 'https://www.example.com/482/2.m4f',
261 | timeline: 0,
262 | uri: '482/2.m4f',
263 | number: 2
264 | }, {
265 | duration: 0.24425000000000008,
266 | map: {
267 | resolvedUri: 'https://www.example.com/482/init.m4f',
268 | uri: '482/init.m4f'
269 | },
270 | resolvedUri: 'https://www.example.com/482/3.m4f',
271 | timeline: 0,
272 | uri: '482/3.m4f',
273 | number: 3
274 | }],
275 | timeline: 0,
276 | uri: ''
277 | }, {
278 | attributes: {
279 | AUDIO: 'audio',
280 | SUBTITLES: 'subs',
281 | BANDWIDTH: 3971000,
282 | CODECS: 'avc1.64001e',
283 | NAME: '720',
284 | ['PROGRAM-ID']: 1,
285 | RESOLUTION: {
286 | height: 404,
287 | width: 720
288 | }
289 | },
290 | contentProtection: {
291 | 'com.widevine.alpha': {
292 | attributes: {
293 | schemeIdUri: 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'
294 | },
295 | pssh: new Uint8Array([181, 235, 45])
296 | }
297 | },
298 | endList: true,
299 | targetDuration: 1.9185833333333333,
300 | mediaSequence: 0,
301 | resolvedUri: '',
302 | segments: [{
303 | duration: 1.9185833333333333,
304 | map: {
305 | resolvedUri: 'https://www.example.com/720/init.m4f',
306 | uri: '720/init.m4f'
307 | },
308 | resolvedUri: 'https://www.example.com/720/0.m4f',
309 | timeline: 0,
310 | uri: '720/0.m4f',
311 | number: 0
312 | }, {
313 | duration: 1.9185833333333333,
314 | map: {
315 | resolvedUri: 'https://www.example.com/720/init.m4f',
316 | uri: '720/init.m4f'
317 | },
318 | resolvedUri: 'https://www.example.com/720/1.m4f',
319 | timeline: 0,
320 | uri: '720/1.m4f',
321 | number: 1
322 | }, {
323 | duration: 1.9185833333333333,
324 | map: {
325 | resolvedUri: 'https://www.example.com/720/init.m4f',
326 | uri: '720/init.m4f'
327 | },
328 | resolvedUri: 'https://www.example.com/720/2.m4f',
329 | timeline: 0,
330 | uri: '720/2.m4f',
331 | number: 2
332 | }, {
333 | duration: 0.24425000000000008,
334 | map: {
335 | resolvedUri: 'https://www.example.com/720/init.m4f',
336 | uri: '720/init.m4f'
337 | },
338 | resolvedUri: 'https://www.example.com/720/3.m4f',
339 | timeline: 0,
340 | uri: '720/3.m4f',
341 | number: 3
342 | }],
343 | timeline: 0,
344 | uri: ''
345 | }],
346 | segments: [],
347 | uri: ''
348 | };
349 |
--------------------------------------------------------------------------------
/mpd-parser/test/manifests/maat_vtt_segmentTemplate.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://www.example.com/base
4 |
5 |
6 |
7 |
8 |
9 | test
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | test
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | test
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | https://example.com/en.vtt
51 |
52 |
53 |
54 |
55 | https://example.com/es.vtt
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/mpd-parser/test/manifests/multiperiod.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | test
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | test
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | test
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | test
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/mpd-parser/test/manifests/segmentBase.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | allowCache: true,
3 | discontinuityStarts: [],
4 | duration: 6,
5 | endList: true,
6 | minimumUpdatePeriod: 0,
7 | mediaGroups: {
8 | 'AUDIO': {},
9 | 'CLOSED-CAPTIONS': {},
10 | 'SUBTITLES': {},
11 | 'VIDEO': {}
12 | },
13 | playlists: [
14 | {
15 | attributes: {
16 | 'AUDIO': 'audio',
17 | 'BANDWIDTH': 449000,
18 | 'CODECS': 'avc1.420015',
19 | 'NAME': '482',
20 | 'PROGRAM-ID': 1,
21 | 'RESOLUTION': {
22 | height: 270,
23 | width: 482
24 | },
25 | 'SUBTITLES': 'subs'
26 | },
27 | endList: true,
28 | resolvedUri: '',
29 | targetDuration: 6,
30 | mediaSequence: 0,
31 | segments: [
32 | {
33 | duration: 6,
34 | timeline: 0,
35 | number: 0,
36 | map: {
37 | uri: '',
38 | resolvedUri: 'https://www.example.com/1080p.ts'
39 | },
40 | resolvedUri: 'https://www.example.com/1080p.ts',
41 | uri: 'https://www.example.com/1080p.ts'
42 | }
43 | ],
44 | timeline: 0,
45 | uri: ''
46 | }
47 | ],
48 | segments: [],
49 | uri: ''
50 | };
51 |
--------------------------------------------------------------------------------
/mpd-parser/test/manifests/segmentBase.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://www.example.com/base
4 |
5 |
6 |
7 |
8 |
9 |
10 | 1080p.ts
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/mpd-parser/test/manifests/segmentList.js:
--------------------------------------------------------------------------------
1 | export const parsedManifest = {
2 | allowCache: true,
3 | discontinuityStarts: [],
4 | duration: 6,
5 | endList: true,
6 | minimumUpdatePeriod: 0,
7 | mediaGroups: {
8 | 'AUDIO': {},
9 | 'CLOSED-CAPTIONS': {},
10 | 'SUBTITLES': {},
11 | 'VIDEO': {}
12 | },
13 | playlists: [
14 | {
15 | attributes: {
16 | 'AUDIO': 'audio',
17 | 'BANDWIDTH': 449000,
18 | 'CODECS': 'avc1.420015',
19 | 'NAME': '482',
20 | 'PROGRAM-ID': 1,
21 | 'RESOLUTION': {
22 | height: 270,
23 | width: 482
24 | },
25 | 'SUBTITLES': 'subs'
26 | },
27 | endList: true,
28 | mediaSequence: 1,
29 | targetDuration: 1,
30 | resolvedUri: '',
31 | segments: [
32 | {
33 | duration: 1,
34 | map: {
35 | uri: '',
36 | resolvedUri: 'https://www.example.com/base'
37 | },
38 | resolvedUri: 'https://www.example.com/low/segment-1.ts',
39 | timeline: 0,
40 | uri: 'low/segment-1.ts',
41 | number: 1
42 | },
43 | {
44 | duration: 1,
45 | map: {
46 | uri: '',
47 | resolvedUri: 'https://www.example.com/base'
48 | },
49 | resolvedUri: 'https://www.example.com/low/segment-2.ts',
50 | timeline: 0,
51 | uri: 'low/segment-2.ts',
52 | number: 2
53 | },
54 | {
55 | duration: 1,
56 | map: {
57 | uri: '',
58 | resolvedUri: 'https://www.example.com/base'
59 | },
60 | resolvedUri: 'https://www.example.com/low/segment-3.ts',
61 | timeline: 0,
62 | uri: 'low/segment-3.ts',
63 | number: 3
64 | },
65 | {
66 | duration: 1,
67 | map: {
68 | uri: '',
69 | resolvedUri: 'https://www.example.com/base'
70 | },
71 | resolvedUri: 'https://www.example.com/low/segment-4.ts',
72 | timeline: 0,
73 | uri: 'low/segment-4.ts',
74 | number: 4
75 | },
76 | {
77 | duration: 1,
78 | map: {
79 | uri: '',
80 | resolvedUri: 'https://www.example.com/base'
81 | },
82 | resolvedUri: 'https://www.example.com/low/segment-5.ts',
83 | timeline: 0,
84 | uri: 'low/segment-5.ts',
85 | number: 5
86 | },
87 | {
88 | duration: 1,
89 | map: {
90 | uri: '',
91 | resolvedUri: 'https://www.example.com/base'
92 | },
93 | resolvedUri: 'https://www.example.com/low/segment-6.ts',
94 | timeline: 0,
95 | uri: 'low/segment-6.ts',
96 | number: 6
97 | }
98 | ],
99 | timeline: 0,
100 | uri: ''
101 | },
102 | {
103 | attributes: {
104 | 'AUDIO': 'audio',
105 | 'BANDWIDTH': 3971000,
106 | 'CODECS': 'avc1.420015',
107 | 'NAME': '720',
108 | 'PROGRAM-ID': 1,
109 | 'RESOLUTION': {
110 | height: 404,
111 | width: 720
112 | },
113 | 'SUBTITLES': 'subs'
114 | },
115 | endList: true,
116 | resolvedUri: '',
117 | mediaSequence: 1,
118 | targetDuration: 60,
119 | segments: [
120 | {
121 | duration: 60,
122 | map: {
123 | uri: '',
124 | resolvedUri: 'https://www.example.com/base'
125 | },
126 | resolvedUri: 'https://www.example.com/high/segment-1.ts',
127 | timeline: 0,
128 | uri: 'high/segment-1.ts',
129 | number: 1
130 | },
131 | {
132 | duration: 60,
133 | map: {
134 | uri: '',
135 | resolvedUri: 'https://www.example.com/base'
136 | },
137 | resolvedUri: 'https://www.example.com/high/segment-2.ts',
138 | timeline: 0,
139 | uri: 'high/segment-2.ts',
140 | number: 2
141 | },
142 | {
143 | duration: 60,
144 | map: {
145 | uri: '',
146 | resolvedUri: 'https://www.example.com/base'
147 | },
148 | resolvedUri: 'https://www.example.com/high/segment-3.ts',
149 | timeline: 0,
150 | uri: 'high/segment-3.ts',
151 | number: 3
152 | },
153 | {
154 | duration: 60,
155 | map: {
156 | uri: '',
157 | resolvedUri: 'https://www.example.com/base'
158 | },
159 | resolvedUri: 'https://www.example.com/high/segment-4.ts',
160 | timeline: 0,
161 | uri: 'high/segment-4.ts',
162 | number: 4
163 | },
164 | {
165 | duration: 60,
166 | map: {
167 | uri: '',
168 | resolvedUri: 'https://www.example.com/base'
169 | },
170 | resolvedUri: 'https://www.example.com/high/segment-5.ts',
171 | timeline: 0,
172 | uri: 'high/segment-5.ts',
173 | number: 5
174 | },
175 | {
176 | duration: 60,
177 | map: {
178 | uri: '',
179 | resolvedUri: 'https://www.example.com/base'
180 | },
181 | resolvedUri: 'https://www.example.com/high/segment-6.ts',
182 | timeline: 0,
183 | uri: 'high/segment-6.ts',
184 | number: 6
185 | },
186 | {
187 | duration: 60,
188 | map: {
189 | uri: '',
190 | resolvedUri: 'https://www.example.com/base'
191 | },
192 | resolvedUri: 'https://www.example.com/high/segment-7.ts',
193 | timeline: 0,
194 | uri: 'high/segment-7.ts',
195 | number: 7
196 | },
197 | {
198 | duration: 60,
199 | map: {
200 | uri: '',
201 | resolvedUri: 'https://www.example.com/base'
202 | },
203 | resolvedUri: 'https://www.example.com/high/segment-8.ts',
204 | timeline: 0,
205 | uri: 'high/segment-8.ts',
206 | number: 8
207 | },
208 | {
209 | duration: 60,
210 | map: {
211 | uri: '',
212 | resolvedUri: 'https://www.example.com/base'
213 | },
214 | resolvedUri: 'https://www.example.com/high/segment-9.ts',
215 | timeline: 0,
216 | uri: 'high/segment-9.ts',
217 | number: 9
218 | },
219 | {
220 | duration: 60,
221 | map: {
222 | uri: '',
223 | resolvedUri: 'https://www.example.com/base'
224 | },
225 | resolvedUri: 'https://www.example.com/high/segment-10.ts',
226 | timeline: 0,
227 | uri: 'high/segment-10.ts',
228 | number: 10
229 | }
230 | ],
231 | timeline: 0,
232 | uri: ''
233 | }
234 | ],
235 | segments: [],
236 | uri: ''
237 | };
238 |
--------------------------------------------------------------------------------
/mpd-parser/test/manifests/segmentList.mpd:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://www.example.com/base
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/mpd-parser/test/parseAttributes.test.js:
--------------------------------------------------------------------------------
1 | import document from 'global/document';
2 | import QUnit from 'qunit';
3 | import { parseAttributes } from '../src/parseAttributes';
4 |
5 | QUnit.module('parseAttributes');
6 |
7 | QUnit.test('simple', function(assert) {
8 | const el = document.createElement('el');
9 |
10 | el.setAttribute('foo', 1);
11 |
12 | assert.deepEqual(parseAttributes(el), { foo: '1' });
13 | });
14 |
15 | QUnit.test('empty', function(assert) {
16 | const el = document.createElement('el');
17 |
18 | assert.deepEqual(parseAttributes(el), {});
19 | });
20 |
--------------------------------------------------------------------------------
/mpd-parser/test/segment/segmentBase.test.js:
--------------------------------------------------------------------------------
1 | import QUnit from 'qunit';
2 | import {
3 | segmentsFromBase,
4 | addSegmentsToPlaylist
5 | } from '../../src/segment/segmentBase';
6 | import errors from '../../src/errors';
7 |
8 | QUnit.module('segmentBase - segmentsFromBase');
9 |
10 | QUnit.test('sets segment to baseUrl', function(assert) {
11 | const inputAttributes = {
12 | baseUrl: 'http://www.example.com/i.fmp4',
13 | initialization: { sourceURL: 'http://www.example.com/init.fmp4' }
14 | };
15 |
16 | assert.deepEqual(segmentsFromBase(inputAttributes), [{
17 | map: {
18 | resolvedUri: 'http://www.example.com/init.fmp4',
19 | uri: 'http://www.example.com/init.fmp4'
20 | },
21 | resolvedUri: 'http://www.example.com/i.fmp4',
22 | uri: 'http://www.example.com/i.fmp4',
23 | number: 0
24 | }]);
25 | });
26 |
27 | QUnit.test('sets duration based on sourceDuration', function(assert) {
28 | const inputAttributes = {
29 | baseUrl: 'http://www.example.com/i.fmp4',
30 | initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
31 | sourceDuration: 10
32 | };
33 |
34 | assert.deepEqual(segmentsFromBase(inputAttributes), [{
35 | duration: 10,
36 | timeline: 0,
37 | map: {
38 | resolvedUri: 'http://www.example.com/init.fmp4',
39 | uri: 'http://www.example.com/init.fmp4'
40 | },
41 | resolvedUri: 'http://www.example.com/i.fmp4',
42 | uri: 'http://www.example.com/i.fmp4',
43 | number: 0
44 | }]);
45 | });
46 |
47 | QUnit.test('sets duration based on sourceDuration and @timescale', function(assert) {
48 | const inputAttributes = {
49 | baseUrl: 'http://www.example.com/i.fmp4',
50 | initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
51 | sourceDuration: 10,
52 | timescale: 2
53 | };
54 |
55 | assert.deepEqual(segmentsFromBase(inputAttributes), [{
56 | duration: 5,
57 | timeline: 0,
58 | map: {
59 | resolvedUri: 'http://www.example.com/init.fmp4',
60 | uri: 'http://www.example.com/init.fmp4'
61 | },
62 | resolvedUri: 'http://www.example.com/i.fmp4',
63 | uri: 'http://www.example.com/i.fmp4',
64 | number: 0
65 | }]);
66 | });
67 |
68 | QUnit.test('sets duration based on @duration', function(assert) {
69 | const inputAttributes = {
70 | duration: 10,
71 | sourceDuration: 20,
72 | baseUrl: 'http://www.example.com/i.fmp4',
73 | initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
74 | periodIndex: 0
75 | };
76 |
77 | assert.deepEqual(segmentsFromBase(inputAttributes), [{
78 | duration: 10,
79 | timeline: 0,
80 | map: {
81 | resolvedUri: 'http://www.example.com/init.fmp4',
82 | uri: 'http://www.example.com/init.fmp4'
83 | },
84 | resolvedUri: 'http://www.example.com/i.fmp4',
85 | uri: 'http://www.example.com/i.fmp4',
86 | number: 0
87 | }]);
88 | });
89 |
90 | QUnit.test('sets duration based on @duration and @timescale', function(assert) {
91 | const inputAttributes = {
92 | duration: 10,
93 | sourceDuration: 20,
94 | timescale: 5,
95 | baseUrl: 'http://www.example.com/i.fmp4',
96 | initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
97 | periodIndex: 0
98 | };
99 |
100 | assert.deepEqual(segmentsFromBase(inputAttributes), [{
101 | duration: 2,
102 | timeline: 0,
103 | map: {
104 | resolvedUri: 'http://www.example.com/init.fmp4',
105 | uri: 'http://www.example.com/init.fmp4'
106 | },
107 | resolvedUri: 'http://www.example.com/i.fmp4',
108 | uri: 'http://www.example.com/i.fmp4',
109 | number: 0
110 | }]);
111 | });
112 |
113 | QUnit.test('translates ranges in node', function(assert) {
114 | const inputAttributes = {
115 | duration: 10,
116 | sourceDuration: 20,
117 | timescale: 5,
118 | baseUrl: 'http://www.example.com/i.fmp4',
119 | initialization: {
120 | sourceURL: 'http://www.example.com/init.fmp4',
121 | range: '121-125'
122 | },
123 | periodIndex: 0
124 | };
125 |
126 | assert.deepEqual(segmentsFromBase(inputAttributes), [{
127 | duration: 2,
128 | timeline: 0,
129 | map: {
130 | resolvedUri: 'http://www.example.com/init.fmp4',
131 | uri: 'http://www.example.com/init.fmp4',
132 | byterange: {
133 | length: 5,
134 | offset: 121
135 | }
136 | },
137 | resolvedUri: 'http://www.example.com/i.fmp4',
138 | uri: 'http://www.example.com/i.fmp4',
139 | number: 0
140 | }]);
141 | });
142 |
143 | QUnit.test('errors if no baseUrl exists', function(assert) {
144 | assert.throws(() => segmentsFromBase({}), new Error(errors.NO_BASE_URL));
145 | });
146 |
147 | QUnit.module('segmentBase - addSegmentsToPlaylist');
148 |
149 | QUnit.test('generates playlist from sidx references', function(assert) {
150 | const baseUrl = 'http://www.example.com/i.fmp4';
151 | const playlist = {
152 | sidx: {
153 | map: {
154 | byterange: {
155 | offset: 0,
156 | length: 10
157 | }
158 | },
159 | duration: 10,
160 | byterange: {
161 | offset: 9,
162 | length: 11
163 | }
164 | },
165 | segments: []
166 | };
167 | const sidx = {
168 | timescale: 1,
169 | firstOffset: 0,
170 | references: [{
171 | referenceType: 0,
172 | referencedSize: 5,
173 | subsegmentDuration: 2
174 | }]
175 | };
176 |
177 | assert.deepEqual(addSegmentsToPlaylist(playlist, sidx, baseUrl).segments, [{
178 | map: {
179 | byterange: {
180 | offset: 0,
181 | length: 10
182 | }
183 | },
184 | uri: 'http://www.example.com/i.fmp4',
185 | resolvedUri: 'http://www.example.com/i.fmp4',
186 | byterange: {
187 | offset: 20,
188 | length: 5
189 | },
190 | duration: 2,
191 | timeline: 0,
192 | number: 0
193 | }]);
194 | });
195 |
--------------------------------------------------------------------------------
/mpd-parser/test/segment/urlType.test.js:
--------------------------------------------------------------------------------
1 | import QUnit from 'qunit';
2 | import {
3 | urlTypeToSegment as urlTypeConverter,
4 | byteRangeToString
5 | } from '../../src/segment/urlType';
6 |
7 | QUnit.module('urlType - urlTypeConverter');
8 |
9 | QUnit.test('returns correct object if given baseUrl only', function(assert) {
10 | assert.deepEqual(urlTypeConverter({ baseUrl: 'http://example.com' }), {
11 | resolvedUri: 'http://example.com',
12 | uri: ''
13 | });
14 | });
15 |
16 | QUnit.test('returns correct object if given baseUrl and source', function(assert) {
17 | assert.deepEqual(urlTypeConverter({
18 | baseUrl: 'http://example.com',
19 | source: 'init.fmp4'
20 | }), {
21 | resolvedUri: 'http://example.com/init.fmp4',
22 | uri: 'init.fmp4'
23 | });
24 | });
25 |
26 | QUnit.test('returns correct object if given baseUrl, source and range', function(assert) {
27 | assert.deepEqual(urlTypeConverter({
28 | baseUrl: 'http://example.com',
29 | source: 'init.fmp4',
30 | range: '101-105'
31 | }), {
32 | resolvedUri: 'http://example.com/init.fmp4',
33 | uri: 'init.fmp4',
34 | byterange: {
35 | offset: 101,
36 | length: 5
37 | }
38 | });
39 | });
40 |
41 | QUnit.test('returns correct object if given baseUrl, source and indexRange', function(assert) {
42 | assert.deepEqual(urlTypeConverter({
43 | baseUrl: 'http://example.com',
44 | source: 'sidx.fmp4',
45 | indexRange: '101-105'
46 | }), {
47 | resolvedUri: 'http://example.com/sidx.fmp4',
48 | uri: 'sidx.fmp4',
49 | byterange: {
50 | offset: 101,
51 | length: 5
52 | }
53 | });
54 | });
55 |
56 | QUnit.test('returns correct object if given baseUrl and range', function(assert) {
57 | assert.deepEqual(urlTypeConverter({
58 | baseUrl: 'http://example.com',
59 | range: '101-105'
60 | }), {
61 | resolvedUri: 'http://example.com',
62 | uri: '',
63 | byterange: {
64 | offset: 101,
65 | length: 5
66 | }
67 | });
68 | });
69 |
70 | QUnit.test('returns correct object if given baseUrl and indexRange', function(assert) {
71 | assert.deepEqual(urlTypeConverter({
72 | baseUrl: 'http://example.com',
73 | indexRange: '101-105'
74 | }), {
75 | resolvedUri: 'http://example.com',
76 | uri: '',
77 | byterange: {
78 | offset: 101,
79 | length: 5
80 | }
81 | });
82 | });
83 |
84 | QUnit.module('urlType - byteRangeToString');
85 |
86 | QUnit.test('returns correct string representing byterange object', function(assert) {
87 | assert.strictEqual(byteRangeToString({
88 | offset: 0,
89 | length: 100
90 | }),
91 | '0-99');
92 | });
93 |
--------------------------------------------------------------------------------
/mpd-parser/test/stringToMpdXml.test.js:
--------------------------------------------------------------------------------
1 | import { stringToMpdXml } from '../src/stringToMpdXml';
2 | import errors from '../src/errors';
3 | import QUnit from 'qunit';
4 |
5 | QUnit.module('stringToMpdXml');
6 |
7 | QUnit.test('simple mpd', function(assert) {
8 | assert.deepEqual(stringToMpdXml('').tagName, 'MPD');
9 | });
10 |
11 | QUnit.test('invalid xml', function(assert) {
12 | assert.throws(() => stringToMpdXml(' stringToMpdXml(''), new RegExp(errors.DASH_INVALID_XML));
17 | });
18 |
19 | QUnit.test('empty manifest', function(assert) {
20 | assert.throws(() => stringToMpdXml(''), new RegExp(errors.DASH_EMPTY_MANIFEST));
21 | });
22 |
--------------------------------------------------------------------------------
/mpd-parser/test/toPlaylists.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | toPlaylists
3 | } from '../src/toPlaylists';
4 | import QUnit from 'qunit';
5 |
6 | QUnit.module('toPlaylists');
7 |
8 | QUnit.test('no representations', function(assert) {
9 | assert.deepEqual(toPlaylists([]), []);
10 | });
11 |
12 | QUnit.test('pretty simple', function(assert) {
13 | const representations = [{
14 | attributes: { baseUrl: 'http://example.com/', periodIndex: 0, sourceDuration: 2 },
15 | segmentInfo: {
16 | template: { }
17 | }
18 | }];
19 |
20 | const playlists = [{
21 | attributes: {
22 | baseUrl: 'http://example.com/',
23 | periodIndex: 0,
24 | sourceDuration: 2,
25 | duration: 2
26 | },
27 | segments: [{
28 | uri: '',
29 | timeline: 0,
30 | duration: 2,
31 | resolvedUri: 'http://example.com/',
32 | map: {
33 | uri: '',
34 | resolvedUri: 'http://example.com/'
35 | },
36 | number: 1
37 | }]
38 | }];
39 |
40 | assert.deepEqual(toPlaylists(representations), playlists);
41 | });
42 |
43 | QUnit.test('segment base', function(assert) {
44 | const representations = [{
45 | attributes: { baseUrl: 'http://example.com/', periodIndex: 0, sourceDuration: 2 },
46 | segmentInfo: {
47 | base: true
48 | }
49 | }];
50 |
51 | const playlists = [{
52 | attributes: {
53 | baseUrl: 'http://example.com/',
54 | periodIndex: 0,
55 | sourceDuration: 2,
56 | duration: 2
57 | },
58 | segments: [{
59 | map: {
60 | resolvedUri: 'http://example.com/',
61 | uri: ''
62 | },
63 | resolvedUri: 'http://example.com/',
64 | uri: 'http://example.com/',
65 | timeline: 0,
66 | duration: 2,
67 | number: 0
68 | }]
69 | }];
70 |
71 | assert.deepEqual(toPlaylists(representations), playlists);
72 | });
73 |
74 | QUnit.test('segment base with sidx', function(assert) {
75 | const representations = [{
76 | attributes: {
77 | baseUrl: 'http://example.com/',
78 | periodIndex: 0,
79 | sourceDuration: 2,
80 | indexRange: '10-19'
81 | },
82 | segmentInfo: {
83 | base: true
84 | }
85 | }];
86 |
87 | const playlists = [{
88 | attributes: {
89 | baseUrl: 'http://example.com/',
90 | periodIndex: 0,
91 | sourceDuration: 2,
92 | duration: 2,
93 | indexRange: '10-19'
94 | },
95 | segments: [],
96 | sidx: {
97 | map: {
98 | resolvedUri: 'http://example.com/',
99 | uri: ''
100 | },
101 | resolvedUri: 'http://example.com/',
102 | uri: 'http://example.com/',
103 | byterange: {
104 | offset: 10,
105 | length: 10
106 | },
107 | timeline: 0,
108 | duration: 2,
109 | number: 0
110 | }
111 | }];
112 |
113 | assert.deepEqual(toPlaylists(representations), playlists);
114 | });
115 |
116 | QUnit.test('segment list', function(assert) {
117 | const representations = [{
118 | attributes: {
119 | baseUrl: 'http://example.com/',
120 | duration: 10,
121 | sourceDuration: 11,
122 | periodIndex: 0
123 | },
124 | segmentInfo: {
125 | list: {
126 | segmentUrls: [{
127 | media: '1.fmp4'
128 | }, {
129 | media: '2.fmp4'
130 | }]
131 | }
132 | }
133 | }];
134 |
135 | const playlists = [{
136 | attributes: {
137 | baseUrl: 'http://example.com/',
138 | duration: 10,
139 | sourceDuration: 11,
140 | segmentUrls: [{
141 | media: '1.fmp4'
142 | }, {
143 | media: '2.fmp4'
144 | }],
145 | periodIndex: 0
146 | },
147 | segments: [{
148 | duration: 10,
149 | map: {
150 | resolvedUri: 'http://example.com/',
151 | uri: ''
152 | },
153 | resolvedUri: 'http://example.com/1.fmp4',
154 | timeline: 0,
155 | uri: '1.fmp4',
156 | number: 1
157 | }, {
158 | duration: 1,
159 | map: {
160 | resolvedUri: 'http://example.com/',
161 | uri: ''
162 | },
163 | resolvedUri: 'http://example.com/2.fmp4',
164 | timeline: 0,
165 | uri: '2.fmp4',
166 | number: 2
167 | }]
168 | }];
169 |
170 | assert.deepEqual(toPlaylists(representations), playlists);
171 | });
172 |
--------------------------------------------------------------------------------
/mpd-parser/test/utils.test.js:
--------------------------------------------------------------------------------
1 | import { merge, values } from '../src/utils/object';
2 | import { parseDuration } from '../src/utils/time';
3 | import { flatten, range, from, findIndexes } from '../src/utils/list';
4 | import { findChildren, getContent } from '../src/utils/xml';
5 | import window from 'global/window';
6 | import document from 'global/document';
7 | import QUnit from 'qunit';
8 |
9 | QUnit.module('utils');
10 |
11 | QUnit.module('merge');
12 | QUnit.test('empty', function(assert) {
13 | assert.deepEqual(merge({}, { a: 1 }), { a: 1 });
14 | assert.deepEqual(merge({ a: 1 }, { a: 1 }), { a: 1 });
15 | assert.deepEqual(merge({ a: 1 }, {}), { a: 1 });
16 | });
17 |
18 | QUnit.test('append', function(assert) {
19 | assert.deepEqual(merge({ a: 1 }, { b: 3 }), { a: 1, b: 3 });
20 | });
21 |
22 | QUnit.test('overwrite', function(assert) {
23 | assert.deepEqual(merge({ a: 1 }, { a: 2 }), { a: 2 });
24 | });
25 |
26 | QUnit.test('empty', function(assert) {
27 | assert.deepEqual(merge({}, {}), {});
28 | assert.deepEqual(merge({}, 1), {});
29 | assert.deepEqual(merge(1, {}), {});
30 | });
31 |
32 | QUnit.test('Test for checking the merge when multiple segment Information are present', function(assert) {
33 |
34 | const adaptationSetInfo = {
35 |
36 | base: { duration: '10'}
37 | };
38 |
39 | const representationInfo = {
40 |
41 | base: { duration: '25', indexRange: '230-252'}
42 | };
43 |
44 | const expected = {
45 |
46 | base: { duration: '25', indexRange: '230-252'}
47 | };
48 |
49 | assert.deepEqual(merge(adaptationSetInfo, representationInfo), expected,
50 | 'Merged SegmentBase info');
51 |
52 | });
53 |
54 | QUnit.test('Test for checking the merge when segment Information is present at a level and is undefined at another', function(assert) {
55 | const periodInfo = {
56 | base: {
57 | initialization: {
58 | range: '0-8888'
59 |
60 | }
61 | }
62 | };
63 |
64 | const adaptationSetInfo = {
65 |
66 | base: { duration: '10', indexRange: '230-252'}
67 | };
68 |
69 | const representationInfo = {};
70 |
71 | const expected = {
72 |
73 | base: { duration: '10', indexRange: '230-252', initialization: {range: '0-8888'}}
74 | };
75 |
76 | assert.deepEqual(merge(periodInfo, adaptationSetInfo, representationInfo), expected,
77 | 'Merged SegmentBase info');
78 |
79 | });
80 |
81 | QUnit.module('values');
82 |
83 | QUnit.test('empty', function(assert) {
84 | assert.deepEqual(values({}), []);
85 | });
86 |
87 | QUnit.test('mixed', function(assert) {
88 | assert.deepEqual(values({ a: 1, b: true, c: 'foo'}), [1, true, 'foo']);
89 | });
90 |
91 | QUnit.module('flatten');
92 | QUnit.test('empty', function(assert) {
93 | assert.deepEqual(flatten([]), []);
94 | });
95 |
96 | QUnit.test('one item', function(assert) {
97 | assert.deepEqual(flatten([[1]]), [1]);
98 | });
99 |
100 | QUnit.test('multiple items', function(assert) {
101 | assert.deepEqual(flatten([[1], [2], [3]]), [1, 2, 3]);
102 | });
103 |
104 | QUnit.test('multiple multiple items', function(assert) {
105 | assert.deepEqual(flatten([[1], [2, 3], [4]]), [1, 2, 3, 4]);
106 | });
107 |
108 | QUnit.test('nested nests', function(assert) {
109 | assert.deepEqual(flatten([[1], [[2]]]), [1, [2]]);
110 | });
111 |
112 | QUnit.test('not a list of lists', function(assert) {
113 | assert.deepEqual(flatten([1, 2]), [1, 2]);
114 | assert.deepEqual(flatten([[1], 2]), [1, 2]);
115 | });
116 |
117 | QUnit.module('parseDuration');
118 | QUnit.test('full date', function(assert) {
119 | assert.deepEqual(parseDuration('P10Y10M10DT10H10M10.1S'), 342180610.1);
120 | });
121 |
122 | QUnit.test('time only', function(assert) {
123 | assert.deepEqual(parseDuration('PT10H10M10.1S'), 36610.1);
124 | });
125 |
126 | QUnit.test('empty', function(assert) {
127 | assert.deepEqual(parseDuration(''), 0);
128 | });
129 |
130 | QUnit.test('invalid', function(assert) {
131 | assert.deepEqual(parseDuration('foo'), 0);
132 | });
133 |
134 | QUnit.module('range');
135 | QUnit.test('simple', function(assert) {
136 | assert.deepEqual(range(1, 4), [1, 2, 3]);
137 | });
138 |
139 | QUnit.test('single number range', function(assert) {
140 | assert.deepEqual(range(1, 1), []);
141 | });
142 |
143 | QUnit.test('negative', function(assert) {
144 | assert.deepEqual(range(-1, 2), [-1, 0, 1]);
145 | });
146 |
147 | QUnit.module('from');
148 |
149 | QUnit.test('simple array', function(assert) {
150 | assert.deepEqual(from([1]), [1]);
151 | });
152 |
153 | QUnit.test('empty array', function(assert) {
154 | assert.deepEqual(from([]), []);
155 | });
156 |
157 | QUnit.test('non-array', function(assert) {
158 | assert.deepEqual(from(1), []);
159 | });
160 |
161 | QUnit.test('array-like', function(assert) {
162 | const fixture = document.createElement('div');
163 |
164 | fixture.innerHTML = '';
165 |
166 | const result = from(fixture.getElementsByTagName('div'));
167 |
168 | assert.ok(result.map);
169 | assert.deepEqual(result.length, 2);
170 | });
171 |
172 | QUnit.module('findIndexes');
173 |
174 | QUnit.test('index not found', function(assert) {
175 | assert.deepEqual(findIndexes([], 'a'), []);
176 | assert.deepEqual(findIndexes([], ''), []);
177 | assert.deepEqual(findIndexes([{ a: true}], 'b'), []);
178 | });
179 |
180 | QUnit.test('indexes found', function(assert) {
181 | assert.deepEqual(findIndexes([{ a: true}], 'a'), [0]);
182 | assert.deepEqual(findIndexes([
183 | { a: true },
184 | { b: true },
185 | { b: true, c: true }
186 | ], 'b'), [1, 2]);
187 | });
188 |
189 | QUnit.module('xml', {
190 | beforeEach() {
191 | const parser = new window.DOMParser();
192 | const xmlString = `
193 |
194 | foo
195 | bar
196 | baz
197 | `;
198 |
199 | this.fixture = parser.parseFromString(xmlString, 'text/xml').documentElement;
200 | }
201 | });
202 |
203 | QUnit.test('findChildren', function(assert) {
204 | assert.deepEqual(findChildren(this.fixture, 'test').length, 1, 'single');
205 | assert.deepEqual(findChildren(this.fixture, 'div').length, 2, 'multiple');
206 | assert.deepEqual(findChildren(this.fixture, 'el').length, 0, 'none');
207 | });
208 |
209 | QUnit.test('getContent', function(assert) {
210 | const result = findChildren(this.fixture, 'test')[0];
211 |
212 | assert.deepEqual(getContent(result), 'foo', 'gets text and trims');
213 | });
214 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Universal-DRM",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/server.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node src/server.js"
9 | },
10 | "apps": [
11 | {
12 | "exec_interpreter": "node",
13 | "name": "auth-app",
14 | "script": "src/server.js",
15 | "post_update": [
16 | "echo App has been updated, running yarn install...",
17 | "yarn install",
18 | "echo App is being restarted now"
19 | ]
20 | }
21 | ],
22 | "author": "DRM-Scripts",
23 | "license": "ISC",
24 | "dependencies": {
25 | "akamai-auth-token": "^1.0.7",
26 | "axios": "^0.19.0",
27 | "basic-auth-connect": "^1.0.0",
28 | "bcrypt": "^3.0.0",
29 | "child-process-promise": "^2.2.1",
30 | "concat": "^1.0.3",
31 | "connect-mongo": "^2.0.1",
32 | "crypto-js": "^3.1.9-1",
33 | "debug-http": "^1.1.0",
34 | "dotenv": "^6.1.0",
35 | "download": "^7.1.0",
36 | "download-file": "^0.1.5",
37 | "es6-promise-pool": "^2.5.0",
38 | "express": "^4.16.3",
39 | "express-ipfilter": "^0.3.1",
40 | "express-session": "^1.15.6",
41 | "fast-glob": "^3.0.3",
42 | "filenamify": "^4.1.0",
43 | "fs-extra": "^8.0.1",
44 | "got": "^9.6.0",
45 | "hls-parser": "^0.1.8",
46 | "http-debug": "^0.1.2",
47 | "jsonfile": "^5.0.0",
48 | "jsonwebtoken": "^8.3.0",
49 | "lint": "^1.1.2",
50 | "m3u8-reader": "^1.1.0",
51 | "m3u8-write": "^1.0.0",
52 | "merge": "^1.2.0",
53 | "mkdirp": "^0.5.1",
54 | "mongodb": "^3.1.1",
55 | "mongoose": "^5.2.2",
56 | "morgan": "^1.9.1",
57 | "mp4-box-encoding": "^1.4.1",
58 | "mp4-stream": "^3.1.0",
59 | "mpd-parser": "file:mpd-parser",
60 | "ncp": "^2.0.0",
61 | "node-fetch": "^2.5.0",
62 | "node-fetch-retry": "^1.0.1",
63 | "notp": "^2.0.3",
64 | "otplib": "^10.0.0",
65 | "portscanner": "^2.2.0",
66 | "promise-request-retry": "^1.0.1",
67 | "promise-retry": "^1.1.1",
68 | "redis": "^2.8.0",
69 | "request": "^2.87.0",
70 | "request-promise": "^4.2.2",
71 | "request-promise-retry": "^1.0.0",
72 | "uuid": "^3.3.2",
73 | "xml2js": "^0.4.19",
74 | "xmldom": "^0.1.27"
75 | },
76 | "devDependencies": {
77 | "eslint": "^5.1.0",
78 | "eslint-config-standard": "^11.0.0",
79 | "eslint-plugin-import": "^2.13.0",
80 | "eslint-plugin-node": "^6.0.1",
81 | "eslint-plugin-promise": "^3.8.0",
82 | "eslint-plugin-standard": "^3.1.0",
83 | "jest": "^24.9.0"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/panels/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Universal-DRM | DRM SCRIPTS
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
107 |
108 |
109 |
110 |
111 |
135 |
136 |
141 |
142 |
143 |
144 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
167 |
168 |
169 |
170 |
171 |
172 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
--------------------------------------------------------------------------------
/panels/start.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
91 |
92 |
103 |
108 |
113 |
114 |
--------------------------------------------------------------------------------
/restart.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | pm2 delete Universal-DRM
3 | pm2 --name Universal-DRM start npm -- start
4 |
--------------------------------------------------------------------------------
/src/server-router.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 | const services = null
4 | const childProcess = require('child_process')
5 |
6 | class ServerRouter {
7 | constructor (util, slingManager) {
8 | this.router = express.Router()
9 | this.util = util
10 | this.slingManager = slingManager
11 | this.router.get('/services', this.getServices.bind(this))
12 | this.router.get('/exo', this.getExo.bind(this))
13 | this.router.get('/test2', this.getExo.bind(this))
14 | this.router.post('/deploy', this.deploy.bind(this))
15 |
16 | this.router.use(bodyParser.json())
17 | this.router.use(this.routeError.bind(this))
18 | }
19 |
20 | getServices (req, res) {
21 | this.util.r(req, res, () => Promise.resolve(services))
22 | }
23 |
24 | getExo (req, res) {
25 | let host = this.util.getReqHost(req)
26 | let promises = [
27 | this.slingManager.getExoSchedule(host)
28 | ]
29 | Promise.all(promises).then(result => {
30 | this.util.r(req, res, () => Promise.resolve([].concat.apply([], result)))
31 | })
32 | }
33 |
34 | deploy (req, res) {
35 | childProcess.exec('./deploy.sh', (err, stdout, stderr) => {
36 | if (err) {
37 | console.log(err)
38 | return res.sendStatus(500)
39 | }
40 | return res.sendStatus(204)
41 | })
42 | }
43 |
44 | routeError (err, req, res, next) {
45 | this.util.routeError(err, req, res, next)
46 | }
47 | }
48 |
49 | module.exports = ServerRouter
50 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const mkdirp = require('mkdirp')
3 | const express = require('express')
4 | // const { promisify } = require('util')
5 | const redis = require('redis')
6 | const bluebird = require('bluebird')
7 | bluebird.promisifyAll(redis.RedisClient.prototype)
8 | const redisClient = redis.createClient(6379, '127.0.0.1')
9 | const bufferRedisClient = redis.createClient(6379, '127.0.0.1', { return_buffers: true })
10 | const basicAuth = require('basic-auth-connect')
11 | const morgan = require('morgan')
12 | const ipfilter = require('express-ipfilter').IpFilter
13 | const ips = ['', '::1', '127.0.0.1']
14 | const debugHttp = require('debug-http')
15 | const fs = require('fs-extra')
16 | const portscanner = require('portscanner')
17 | redisClient.keys('video*', (err, rows) => {
18 | if (err) console.error('Error from flushCache: ', err)
19 | for (let row of rows) redisClient.del(row)
20 | })
21 | fs.emptyDirSync('public')
22 | fs.removeSync('video')
23 | mkdirp.sync('video')
24 |
25 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0
26 | debugHttp()
27 |
28 | const SlingRouter = require('./services/sling-router')
29 | const SlingManager = require('./services/sling-manager')
30 |
31 | const DSTVRouter = require('./services/dstv-router')
32 | const DSTVManager = require('./services/dstv-manager')
33 |
34 | const DTVNowRouter = require('./services/dtv-now-router')
35 | const DTVNowManager = require('./services/dtv-now-manager')
36 |
37 | const LiveRouter = require('./services/live-router')
38 | const LiveManager = require('./services/live-manager')
39 |
40 | const RapRouter = require('./services/rap-router')
41 | const RapManager = require('./services/rap-manager')
42 |
43 | const TataSkyRouter = require('./services/tatasky-router')
44 | const TataSkyManager = require('./services/tatasky-manager')
45 |
46 | const TSNRouter = require('./services/tsn-router')
47 | const TSNManager = require('./services/tsn-manager')
48 |
49 | const BellRouter = require('./services/bell-router')
50 | const BellManager = require('./services/bell-manager')
51 |
52 | const ServerRouter = require('./server-router')
53 |
54 | const VideoManager = require('./utils/video-manager')
55 | const VideoRouter = require('./utils/video-router')
56 |
57 | const Util = require('./utils/util')
58 |
59 | class Server {
60 | constructor () {
61 | this.socketConnections = []
62 | this.serverPort = null
63 | }
64 |
65 | findPort () {
66 | return new Promise((resolve, reject) => {
67 | portscanner.findAPortNotInUse(3001, 3020, '127.0.0.1', (error, port) => {
68 | if (error) reject(new Error('no ports found!'))
69 | console.log('AVAILABLE PORT AT: ' + port)
70 | resolve(port)
71 | })
72 | })
73 | }
74 |
75 | async start () {
76 | this.serverPort = await this.findPort()
77 | this.startServer()
78 | console.log(`\n\tRunning ${process.env.GAE_INSTANCE} auth-service at http://localhost:${this.serverPort}\n`)
79 | }
80 |
81 | startServer () {
82 |
83 | const videoManager = new VideoManager(new Util('video-manager'), redisClient, bufferRedisClient)
84 |
85 | const slingManager = new SlingManager(redisClient, new Util('sling-manager'), videoManager)
86 | const slingRouter = new SlingRouter(slingManager, new Util('sling-router'), videoManager)
87 |
88 | const dstvManager = new DSTVManager(new Util('dstv-manager'), redisClient, videoManager)
89 | const dstvRouter = new DSTVRouter(dstvManager, new Util('dstv-router'), videoManager)
90 |
91 | const dtvnowManager = new DTVNowManager(new Util('dtv-now-manager'), redisClient, videoManager)
92 | const dtvnowRouter = new DTVNowRouter(dtvnowManager, new Util('dtv-now-router'), videoManager)
93 |
94 | const liveManager = new LiveManager(new Util('live-manager'), redisClient, videoManager)
95 | const liveRouter = new LiveRouter(liveManager, new Util('live-router'), videoManager)
96 |
97 | const rapManager = new RapManager(new Util('rap-manager'), redisClient, videoManager)
98 | const rapRouter = new RapRouter(rapManager, new Util('rap-router'), videoManager)
99 |
100 | const tataskyManager = new TataSkyManager(new Util('tatasky-manager'), redisClient, videoManager)
101 | const tataskyRouter = new TataSkyRouter(tataskyManager, new Util('tatasky-router'), videoManager)
102 |
103 | const tsnManager = new TSNManager(new Util('tsn-manager'), redisClient, videoManager)
104 | const tsnRouter = new TSNRouter(tsnManager, new Util('tsn-router'), videoManager)
105 |
106 | const bellManager = new BellManager(redisClient, new Util('bell-manager'))
107 | const bellRouter = new BellRouter(bellManager, new Util('bell-router'))
108 |
109 | const videoRouter = new VideoRouter(new Util('video-router'), videoManager)
110 | const serverRouter = new ServerRouter(new Util('server-router'), slingManager)
111 |
112 | const app = express()
113 | app.set('trust proxy', true)
114 |
115 | app.use(morgan(':date[iso] :method :url :status :res[content-length] - :response-time[0] ms'))
116 |
117 | app.use((req, res, next) => {
118 | res.header('Access-Control-Allow-Origin', '*')
119 |
120 | if (req.method === 'OPTIONS') {
121 | res.sendStatus(200)
122 | } else {
123 | next()
124 | }
125 | })
126 | app.use('/static', ipfilter(ips, { mode: 'allow', logLevel: 'deny' }))
127 | app.use('/static', express.static('public'))
128 | app.use('/sec', express.static('public'))
129 | app.use('/secret', basicAuth('sinep', 'sllab'))
130 | app.use('/secret', express.static('panels'))
131 |
132 | app.use('/sling', slingRouter.router)
133 | app.use('/dstv', dstvRouter.router)
134 | app.use('/bell', bellRouter.router)
135 | app.use('/rap', rapRouter.router)
136 | app.use('/dtv-now', dtvnowRouter.router)
137 | app.use('/tsn', tsnRouter.router)
138 | app.use('/tatasky', tataskyRouter.router)
139 | app.use('/live', liveRouter.router)
140 |
141 |
142 | app.use('/video', videoRouter.router)
143 |
144 | app.use('/', serverRouter.router)
145 |
146 | this.server = app.listen(this.serverPort)
147 | this.server.on('connection', socket => {
148 | this.socketConnections.push(socket)
149 |
150 | socket.on('close', () => {
151 | this.socketConnections = this.socketConnections.filter(sock => sock !== socket)
152 | })
153 | })
154 | }
155 | }
156 |
157 | const server = new Server()
158 | server.start()
159 |
--------------------------------------------------------------------------------
/src/services/bell-manager.js:
--------------------------------------------------------------------------------
1 | var rp = require('request-promise')
2 | const deviceId = process.env.BELL_DEVICEID
3 | const username = process.env.BELL_USER
4 | const password = process.env.BELL_PASS
5 | const proxy = undefined
6 | const querystring = require('query-string')
7 |
8 | class BellManager {
9 | constructor (redisClient, util, videoManager) {
10 | this.util = util
11 | this.videoManager = videoManager
12 | this.redisClient = redisClient
13 | this.headers = {
14 | 'X-Bell-UDID': deviceId,
15 | 'X-Bell-API-Key': ''
16 | }
17 | }
18 |
19 | async login () {
20 | let cacheId = 'bell_login'
21 | let loginData = await this.redisClient.getAsync(cacheId)
22 | if (loginData) {
23 | return JSON.parse(loginData)
24 | }
25 |
26 | let body = {
27 | 'accessNetwork': 'WIFI',
28 | 'useWifiAuth': true,
29 | 'bmSubId': null,
30 | 'sSubId': null,
31 | 'useMobileAuth': true,
32 | 'imsi': null,
33 | 'sImsi': null,
34 | 'mobileOperator': '',
35 | 'cellTowerOperator': null,
36 | 'pairingAuthTokens': [],
37 | 'username': username,
38 | 'password': password,
39 | 'credentialsToken': null,
40 | 'ssoToken': null,
41 | 'guestChannelMap': null,
42 | 'location': null,
43 | 'device': {
44 | 'platform': 'Mac OS',
45 | 'model': 'Chrome 75.0.2819.101',
46 | 'name': 'Chrome - Mac OS',
47 | 'version': '10.12.1',
48 | 'language': 'en',
49 | 'additionalInformations': []
50 | },
51 | 'client': {
52 | 'name': 'fonse-web',
53 | 'version': '7.3.21'
54 | },
55 | 'organization': 'bell'
56 | }
57 | loginData = await rp.post('https://vcm-origin.nscreen.iptv.bell.ca/api/authnz/v3/session', { headers: this.headers, proxy, body, json: true })
58 | this.redisClient.setex(cacheId, 10000, JSON.stringify(loginData))
59 |
60 | return loginData
61 | }
62 |
63 | async getChannels () {
64 | let entitlementData = await this.login()
65 | let callsigns = entitlementData.tvAccounts[0].epgSubscriptions.callSigns
66 | let channelData = await rp.get('https://tv.bell.ca/api/epg/v3/channels?epgChannelMap=MAP_TORONTO&epgVersion=97197&tvService=fibe', { headers: this.headers, json: true })
67 |
68 | for (let i = channelData.length - 1; i >= 0; i--) {
69 | let channel = channelData[i]
70 | if (!callsigns.includes(channel.callSign)) {
71 | channelData.splice(i, 1)
72 | } else {
73 | channel.url = `http://localhost:3001/video/master.m3u8?${querystring.stringify({ mpd: `http://localhost:3001/bell/${channel.callSign}.mpd`, licenseUrl: `http://localhost:3001/bell/widevine?callsign=${channel.callSign}` })}`
74 | }
75 | }
76 |
77 | return channelData
78 | }
79 |
80 | async getStreamData (callsign) {
81 | let entitlementData = await this.login()
82 | let newHeaders = {
83 | 'X-Bell-CToken': entitlementData.ctoken,
84 | 'X-Bell-Player-Agent': 'fonse-web/7.3.21 AMC/2.11.1_45235-mirego12-6.3.swf;Native/8.9.0 (dynamicAdInsertion;widevine)'
85 | }
86 | let headers = Object.assign(newHeaders, this.headers)
87 | let body = { assetId: callsign, type: 'LIVE', mergedTvAccounts: [] }
88 | let resp = await rp.post(`https://vcm-origin.nscreen.iptv.bell.ca/api/playback/v3/tvAccounts/${entitlementData.tvAccounts[0].id}/streamings?warmup=true`, { body, proxy, headers, json: true })
89 | // let resp = await rp.put(`https://vcm-origin.nscreen.iptv.bell.ca/api/playback/v3/tvAccounts/${entitlementData.tvAccounts[0].id}/streamings/${warmup.streamingId}`, { body, proxy, headers, json: true })
90 | return resp
91 | }
92 |
93 | async getStreamUrl (callsign) {
94 | let streamData = await this.getStreamData(callsign)
95 | return Promise.resolve({ url: streamData.policies[0].player.streamingUrl })
96 | }
97 |
98 | async processWidevine (body, callsign) {
99 | let entitlementData = await this.login()
100 | let streamData = await this.getStreamData(callsign)
101 | let newHeaders = {
102 | 'Host': 'vcm-origin.nscreen.iptv.bell.ca',
103 | 'X-Bell-Play-Token': streamData.policies[0].playToken,
104 | 'Origin': 'https://tv.bell.ca',
105 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36',
106 | 'Sec-Fetch-Mode': 'cors',
107 | 'X-Bell-CToken': entitlementData.ctoken,
108 | 'X-Bell-Player-Agent': 'fonse-web/7.3.21 AMC/2.11.1_45235-mirego12-6.3.swf;Native/8.9.0 (dynamicAdInsertion;widevine)',
109 | 'Accept': '*/*',
110 | 'Sec-Fetch-Site': 'same-site',
111 | 'Accept-Language': 'en-US,en;q=0.9',
112 | 'Pragma': 'no-cache',
113 | 'Cache-Control': 'no-cache'
114 | }
115 | let headers = Object.assign(newHeaders, this.headers)
116 | if (Object.keys(body).length === 0) body = '\x08\x04'
117 | let widevineUrl = 'https://vcm-origin.nscreen.iptv.bell.ca/api/license/v1/widevine/request'
118 | let options = { uri: widevineUrl, headers, proxy, body, encoding: null }
119 | return rp.post(options)
120 | }
121 | }
122 |
123 | module.exports = BellManager
124 |
--------------------------------------------------------------------------------
/src/services/bell-router.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 |
4 | class BellRouter {
5 | constructor (bellManager, util) {
6 | this.router = express.Router()
7 | this.util = util
8 | this.bellManager = bellManager
9 |
10 | this.router.use(bodyParser.raw({ type: function (req) { return true } }))
11 | this.router.get('/:id.mpd', this.getStream.bind(this))
12 | this.router.post('/widevine', this.processWidevine.bind(this))
13 | this.router.get('/scheduleStatus.json', this.channels.bind(this))
14 |
15 | this.router.use(this.routeError.bind(this))
16 | }
17 |
18 | processWidevine (req, res) {
19 | res.setHeader('Content-Type', 'application/octet-stream')
20 | this.util.r(req, res, () => this.bellManager.processWidevine(req.body, req.query.callsign))
21 | }
22 |
23 | channels (req, res) {
24 | this.util.r(req, res, () => this.bellManager.getChannels())
25 | }
26 |
27 | getStream (req, res) {
28 | this.bellManager.getStreamUrl(req.params.id).then(streamData => {
29 | res.redirect(streamData.url)
30 | })
31 | }
32 |
33 | routeError (err, req, res, next) {
34 | this.util.routeError(err, req, res, next)
35 | }
36 | }
37 |
38 | module.exports = BellRouter
39 |
--------------------------------------------------------------------------------
/src/services/dstv-manager.js:
--------------------------------------------------------------------------------
1 | var rp = require('request-promise')
2 | const mpdParser = require('mpd-parser')
3 | const url = require('url')
4 | const fetch = require('node-fetch')
5 | let scheduleUrl = 'https://ssl.dstv.com/api/cs-mobile/epg/v4/channelsByCountryAndPackage;country=ZA;subscriptionPackage=PREMIUM;eventsCount=1'
6 | let ip = ''
7 |
8 | class DSTVManager {
9 | constructor (util, redisClient, videoManager) {
10 | this.util = util
11 | this.videoManager = videoManager
12 | this.redisClient = redisClient
13 | this.config = {}
14 | }
15 |
16 | async getStream (id) {
17 | let schedule = await this.getSchedule()
18 | let channelItem = schedule.find(x => x.id === id)
19 | let streamItem = channelItem.streams.find(x => x.streamType === 'WebAlt')
20 | let streamUrl = streamItem.playerUrl
21 | return `${streamUrl.split('?')[0]}/.mpd`
22 | }
23 |
24 | async processWidevine (body) {
25 | let sessionTokens = await this.licenseAuth()
26 | let options = {
27 | url: 'https://foxtelott.live.ott.irdeto.com/widevine/getlicense',
28 | qs: {
29 | CrmId: 'afl',
30 | AccountId: 'afl',
31 | ContentId: 'SPY',
32 | SessionId: sessionTokens.sessionId,
33 | Ticket: sessionTokens.ticket
34 | },
35 | body: body,
36 | headers: {
37 | 'X-Forwarded-For': ip
38 | },
39 | encoding: null
40 | }
41 | return rp.post(options)
42 | }
43 |
44 | async login () {
45 | let headers = {
46 | 'content-type': 'application/x-www-form-urlencoded'
47 | }
48 |
49 | let params = 'userCountry=South_Africa&redirect_uri=http%3A%2F%2Flocalhost%3A49154%2F&nonce=1234&client_id=cf747925-dd85-4dde-b4f3-61dfe94ff517&response_type=id_token+token&scope=openid&AuthenticationType=Email&Email=email%40email.co.za&Mobile=&Password=password'
50 | let loginResp = await rp.post({ uri: 'https://connect.dstv.com/4.1/DStvNowApp/OAuth/Login', headers: headers, body: params, simple: false })
51 | return /access_token=(.*?)&/.exec(loginResp)[1]
52 | }
53 |
54 | async licenseAuth () {
55 | let authToken = await this.login()
56 | let headers = {
57 | 'authorization': authToken,
58 | 'x-forwarded-for': ip
59 | }
60 |
61 | let options = {
62 | headers: headers,
63 | method: 'POST',
64 | body: '{}'
65 | }
66 | let auth = await fetch('https://ssl.dstv.com/api/cs-mobile/user-manager/v4/vod-authorisation;productId=1b09957b-27aa-493b-a7c9-53b3cec92d63;platformId=32faad53-5e7b-4cc0-9f33-000092e85950;deviceType=Web', options).then(res => res.json())
67 | return auth.irdetoSession
68 | }
69 |
70 | async getScheduleStatus () {
71 | let scheduleData = await this.getSchedule()
72 | if (this.videoManager.config) {
73 | for (let channel of scheduleData) {
74 | let cacheId = `dstv-${channel.guid}`
75 | channel.active = !!(this.videoManager.config[cacheId] && this.videoManager.config[cacheId].started)
76 | channel.startTime = this.videoManager.config[cacheId] ? this.videoManager.config[cacheId].startTime : null
77 | }
78 | }
79 | return scheduleData
80 | }
81 |
82 | getSegments () {
83 | let self = this
84 | return async function processSegments (id) {
85 | id = id.split(/-(.+)/)[1]
86 | let mpd = await self.getStream(id)
87 | let manifest = await rp.get({ uri: mpd, headers: { 'X-Forwarded-For': ip } })
88 | let parsedManifest = mpdParser.parse(manifest, mpd)
89 | let lastSegment = await self.redisClient.getAsync(`video.${id}.lastSegment`)
90 | let lastSegmentAudio = await self.redisClient.getAsync(`video.${id}.lastSegmentAudio`)
91 | let playlists = self.util.getMaxPlaylists(parsedManifest)
92 | let pssh = playlists.maxVideo.contentProtection['com.widevine.alpha'].psshNormal
93 | let max = playlists.maxVideo
94 | let maxAudio = playlists.maxAudio
95 | let lastSegmentIndex = 0
96 | let lastSegmentAudioIndex = 0
97 | if (lastSegment) lastSegmentIndex = max.segments.findIndex(x => x.uri === lastSegment)
98 | if (lastSegmentAudio) lastSegmentAudioIndex = maxAudio.segments.findIndex(x => x.uri === lastSegmentAudio)
99 | if (lastSegment === -1 || lastSegmentAudio === -1) throw new Error('couldnt find index of last segment')
100 | let trimmed = max.segments.slice(-Math.abs(max.segments.length - 1 - lastSegmentIndex))
101 | let trimmedAudio = maxAudio.segments.slice(-Math.abs(maxAudio.segments.length - 1 - lastSegmentAudioIndex))
102 | let segments = [url.resolve(mpd, trimmed[0].map.uri)]
103 | let audio = [url.resolve(mpd, trimmedAudio[0].map.uri)]
104 | let maxLastAudio = 60
105 | let maxLastVideo = 60
106 | for (let segment of trimmed) {
107 | if (!lastSegmentIndex) maxLastVideo = maxLastVideo - segment.duration
108 | segments.push(url.resolve(mpd, segment.uri))
109 | if (maxLastVideo < 0) break
110 | }
111 | for (let segmentAudio of trimmedAudio) {
112 | if (!lastSegmentAudioIndex) maxLastAudio = maxLastAudio - segmentAudio.duration
113 | audio.push(url.resolve(mpd, segmentAudio.uri))
114 | if (maxLastAudio < 0) break
115 | }
116 | let finalSegments = { segments: {}, audio: {} }
117 | finalSegments.segments[pssh] = segments
118 | finalSegments.audio[pssh] = audio
119 | self.redisClient.set(`video.${id}.lastSegment`, max.segments[max.segments.length - 1].uri)
120 | self.redisClient.set(`video.${id}.lastSegmentAudio`, maxAudio.segments[maxAudio.segments.length - 1].uri)
121 | return Promise.resolve({ segments: finalSegments.segments, audio: finalSegments.audio })
122 | }
123 | }
124 |
125 | async getSchedule () {
126 | let redisSchedule = await this.redisClient.getAsync(`dstv-schedule`)
127 | let scheduleData = null
128 | if (redisSchedule) {
129 | scheduleData = JSON.parse(await this.redisClient.getAsync(`dstv-schedule`))
130 | } else {
131 | scheduleData = await fetch(scheduleUrl, { headers: { 'X-Forwarded-For': ip } }).then(res => res.json())
132 | for (let item of scheduleData.items) {
133 | item.channel_name = item.channelName
134 | item.assetTitle = item.events && item.events.length > 0 ? item.events[0].title : ''
135 | item.guid = item.id
136 | }
137 | this.redisClient.setex(`dstv-schedule`, 3600, JSON.stringify(scheduleData))
138 | }
139 |
140 | return scheduleData.items
141 | }
142 | }
143 |
144 | module.exports = DSTVManager
145 |
--------------------------------------------------------------------------------
/src/services/dstv-router.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 | let root = 'dstv'
4 |
5 | class DSTVRouter {
6 | constructor (dstvManager, util, videoManager) {
7 | this.router = express.Router()
8 | this.util = util
9 | this.dstvManager = dstvManager
10 | this.videoManager = videoManager
11 | this.router.use(bodyParser.raw({ type: function (req) { return true } }))
12 | this.router.post('/widevine', this.processWidevine.bind(this))
13 | this.router.get('/scheduleStatus.json', this.getScheduleStatus.bind(this))
14 | this.router.get('/build', this.build.bind(this))
15 | this.router.use(this.routeError.bind(this))
16 | }
17 |
18 | build (req, res) {
19 | this.util.r(req, res, () => this.videoManager.startBuild(`${root}-${req.query.channelId}`, 'http://localhost:3001/dstv/widevine', this.dstvManager.getSegments()))
20 | }
21 |
22 | processWidevine (req, res) {
23 | this.util.r(req, res, () => this.dstvManager.processWidevine(req.body))
24 | }
25 |
26 | getScheduleStatus (req, res) {
27 | this.util.r(req, res, () => this.dstvManager.getScheduleStatus())
28 | }
29 |
30 | routeError (err, req, res, next) {
31 | this.util.routeError(err, req, res, next)
32 | }
33 | }
34 |
35 | module.exports = DSTVRouter
36 |
--------------------------------------------------------------------------------
/src/services/dtv-now-manager.js:
--------------------------------------------------------------------------------
1 | var rp = require('request-promise')
2 | const querystring = require('querystring')
3 | const widevineUrl = 'https://api.cld.dtvce.com/rights/management/mdrm/vgemultidrm/v1/widevine/license'
4 | const activateUrl = 'https://api.cld.dtvce.com/rights/management/mdrm/vgemultidrm/v1/widevine/activate'
5 | let j = rp.jar()
6 |
7 | class DTVNowManager {
8 | constructor (redisClient, util, videoManager) {
9 | this.util = util
10 | this.videoManager = videoManager
11 | this.redisClient = redisClient
12 | }
13 |
14 | async login () {
15 | let cacheId = 'dtv-now-login'
16 | let cache = await this.redisClient.getAsync(cacheId)
17 | if (cache) return JSON.parse(cache)
18 | let headers = {
19 | 'Host': 'cprodmasx.att.com',
20 | 'Pragma': 'no-cache',
21 | 'Cache-Control': 'no-cache',
22 | 'Origin': 'https://cprodmasx.att.com',
23 | 'Upgrade-Insecure-Requests': '1',
24 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36',
25 | 'Sec-Fetch-Mode': 'navigate',
26 | 'Sec-Fetch-User': '?1',
27 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
28 | 'Sec-Fetch-Site': 'same-origin',
29 | 'Referer': 'https://cprodmasx.att.com/commonLogin/igate_wam/controller.do?TAM_OP=login&USERNAME=unauthenticated&ERROR_CODE=0x00000000&ERROR_TEXT=HPDBA0521I%20%20%20Successful%20completion&METHOD=GET&URL=%2Fpkmsvouchfor%3FATT%26https%3A%2F%2Fcprodx.att.com%2FTokenService%2FnxsATS%2FWATokenService%3FisPassive%3Dfalse%26lang%3Den%26appID%3Dm14961%26returnURL%3Dhttps%253A%252F%252Fapi.cld.dtvce.com%252Faccount%252Faeg%252Fums%252Ftglogin%253FnextUrl%253Dhttps%25253A%25252F%25252Fwww.directvnow.com%25252Faccounts%25252Fsign-in&REFERER=https%3A%2F%2Fcprodmasx.att.com%2FcommonLogin%2Figate_wam%2Fcontroller.do%3FTAM_OP%3Dlogout%26USERNAME%3D%26ERROR_CODE%3D0x00000000%26ERROR_TEXT%3DSuccessful%2520completion%26METHOD%3DGET%26URL%3D%2Fpkmslogout%26REFERER%3D%26AUTHNLEVEL%3D%26FAILREASON%3D%26OLDSESSION%3D%26style%3DTokenService%26returnurl%3Dhttps%253A%252F%252Fcprodx.att.com%252FTokenService%252FnxsATS%252FWATokenService%253FisPassive%253Dfalse%2526lang%253Den%2526appID%253Dm14961%2526returnURL%253Dhttps%25253A%25252F%25252Fapi.cld.dtvce.com%25252Faccount%25252Faeg%25252Fums%25252Ftglogin%25253FnextUrl%25253Dhttps%2525253A%2525252F%2525252Fwww.directvnow.com%2525252Faccounts%2525252Fsign-in&HOSTNAME=cprodmasx.att.com&AUTHNLEVEL=&FAILREASON=&OLDSESSION=',
30 | 'Accept-Language': 'en-US,en;q=0.9'
31 | }
32 |
33 | let dataString = {
34 | userid: process.env.DTVNOW_USER,
35 | password: process.env.DTVNOW_PASS,
36 | cancelURL: 'https://cprodmasx.att.com/commonLogin/igate_wam/controller.do?TAM_OP=login&USERNAME=unauthenticated&ERROR_CODE=0x00000000&ERROR_TEXT=HPDBA0521I%20%20%20Successful%20completion&METHOD=GET&URL=%2Fpkmsvouchfor%3FATT%26https%3A%2F%2Fcprodx.att.com%2FTokenService%2FnxsATS%2FWATokenService%3FisPassive%3Dfalse%26lang%3Den%26appID%3Dm14961%26returnURL%3Dhttps%253A%252F%252Fapi.cld.dtvce.com%252Faccount%252Faeg%252Fums%252Ftglogin%253FnextUrl%253Dhttps%25253A%25252F%25252Fwww.atttvnow.com%25252Faccounts%25252Fsign-in&REFERER=https%3A%2F%2Fwww.atttvnow.com%2Faccounts%2Fsign-in&HOSTNAME=cprodmasx.att.com&AUTHNLEVEL=&FAILREASON=&OLDSESSION=',
37 | remember_me: 'Y',
38 | source: 'm14961',
39 | loginURL: '/WEB-INF/pages/directvNow/dtvNowLoginWeb.jsp',
40 | targetURL: '/pkmsvouchfor?ATT&https://cprodx.att.com/TokenService/nxsATS/WATokenService?isPassive=false&lang=en&appID=m14961&returnURL=https%3A%2F%2Fapi.cld.dtvce.com%2Faccount%2Faeg%2Fums%2Ftglogin%3FnextUrl%3Dhttps%253A%252F%252Fwww.atttvnow.com%252Faccounts%252Fsign-in',
41 | appID: 'm14961',
42 | HOSTNAME: 'cprodmasx.att.com',
43 | tGSignInOptURL: 'https://m.att.com/my/#/forgotLoginLanding?origination_point=dtvmig&Flow_Indicator=FPWD&Return_URL=https%3A%2F%2Fcprodx.att.com%2FTokenService%2FnxsATS%2FWATokenService%3FisPassive%3Dfalse%26lang%3Den%26appID%3Dm14961%26returnURL%3Dhttps%253A%252F%252Fapi.cld.dtvce.com%252Faccount%252Faeg%252Fums%252Ftglogin%253FnextUrl%253Dhttps%25253A%25252F%25252Fwww.atttvnow.com%25252Faccounts%25252Fsign-in',
44 | style: 'm14961'
45 | }
46 |
47 | let options = {
48 | url: 'https://cprodmasx.att.com/commonLogin/igate_wam/multiLogin.do',
49 | method: 'POST',
50 | jar: j,
51 | headers: headers,
52 | form: dataString,
53 | followAllRedirects: true
54 | }
55 | let result = await rp.post(options)
56 | let tats = /input type="hidden" name="TATS-TokenID" value="(.+?)"/.exec(result)[1]
57 |
58 | headers = {
59 | 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
60 | 'content-type': 'application/x-www-form-urlencoded',
61 | 'origin': 'https://cprodx.att.com',
62 | 'content-length': '461',
63 | 'accept-language': 'en-us',
64 | 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15',
65 | 'referer': 'https://cprodx.att.com/TokenService/nxsATS/WATokenService?isPassive=false&lang=en&appID=m14961&returnURL=https%3A%2F%2Fapi.cld.dtvce.com%2Faccount%2Faeg%2Fums%2Ftglogin%3FnextUrl%3Dhttps%253A%252F%252Fwww.atttvnow.com%252Faccounts%252Fsign-in',
66 | 'Pragma': 'no-cache',
67 | 'Cache-Control': 'no-cache'
68 | }
69 |
70 | options = {
71 | jar: j,
72 | headers: headers,
73 | form: { 'TATS-TokenID': tats },
74 | followAllRedirects: true
75 | }
76 |
77 | await rp.post('https://api.cld.dtvce.com/account/aeg/ums/tglogin?nextUrl=https%3A%2F%2Fwww.atttvnow.com%2Faccounts%2Fsign-in', options)
78 |
79 | let authResult = await rp.post('https://www.att.tv/auth', options)
80 | let accessToken = /accessToken:"(.+?)"/.exec(authResult)[1]
81 | let activationToken = /activationToken:"(.+?)"/.exec(authResult)[1]
82 | activationToken = Buffer.from(activationToken, 'hex').toString('base64')
83 | let loginObj = { accessToken, activationToken }
84 | this.redisClient.setex(cacheId, 3500, JSON.stringify(loginObj))
85 | return Promise.resolve({ accessToken, activationToken })
86 | }
87 |
88 | async getSchedule () {
89 | let loginData = await this.login()
90 | let headers = {
91 | Authorization: `Bearer ${loginData.accessToken}`
92 | }
93 | let channels = await rp.get('https://api.cld.dtvce.com/discovery/metadata/channel/v1/service/channel?uxReference=CHANNEL.SEARCH&itemCount=999&itemIndex=0', { headers, json: true }).then(x => x.channels)
94 | let promises = []
95 | for (let channel of channels) {
96 | promises.push(this.getChannelMeta(channel.ccId)
97 | .then(meta => {
98 | if (meta.dRights) {
99 | let licenseUrl = `http://localhost:3001/dtvnow/widevine?${querystring.stringify({ contentId: channel.ccId })}`
100 | let mpd = meta.playbackData.fallbackStreamUrl
101 | .replace('_mobile.mpd', '.mpd')
102 | .replace('index_mobile.m3u8', 'manifest.mpd')
103 | .replace('HLS.abre', 'DASH.abre')
104 | channel.statement = `http://localhost:3001/video/master.m3u8?${querystring.stringify({ licenseUrl, mpd })}`
105 | } else {
106 | console.warn(`no rights for ${channel.ccId}`)
107 | }
108 | })
109 | .catch(err => {
110 | console.warn(`no rights for ${channel.ccId}, err: ${err}`)
111 | }))
112 | }
113 | return Promise.all(promises).then(() => channels)
114 | }
115 |
116 | async getChannelMeta (ccid, useCache = true) {
117 | let cacheId = `dtvmeta-${ccid}`
118 | let cache = await this.redisClient.getAsync(cacheId)
119 | if (cache && useCache) {
120 | return JSON.parse(cache)
121 | } else {
122 | let loginData = await this.login()
123 | let headers = {
124 | Authorization: `Bearer ${loginData.accessToken}`,
125 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36'
126 | }
127 | return rp.get(`https://api.cld.dtvce.com/right/authorization/channel/v1?ccid=${ccid}&clientContext=proximity:outofhome,dmaID:803_0,billingDmaID:803,regionID:PRIMEHD_BGTN4HD_FSWHD_BGTN3HD_BIG10HD_PRIMHD_BG10O2H,zipCode:92345,countyCode:071,stateNumber:6,stateAbbr:CA,pkgCode:DVR%2020%20hours_Ultimate&proximity=O&reserveCTicket=true`, { headers, json: true }).then(resp => {
128 | if (resp.dRights) this.redisClient.setex(cacheId, 86400 * 30, JSON.stringify(resp))
129 | return resp
130 | })
131 | }
132 | }
133 |
134 | async processWidevine (body, contentId) {
135 | let loginData = await this.login()
136 | let headers = {
137 | Authorization: `Bearer ${loginData.accessToken}`
138 | }
139 | let challenge = body.toString('base64')
140 | let activateBody = {
141 | activationToken: loginData.activationToken,
142 | activationChallenge: challenge
143 | }
144 | let identityCookie = await this.redisClient.getAsync(activateUrl)
145 | if (!identityCookie) {
146 | let activationData = await rp.post({ url: activateUrl, headers: headers, body: activateBody, json: true })
147 | identityCookie = activationData.identityCookie
148 | this.redisClient.setex(activateUrl, 86400, identityCookie)
149 | }
150 | let meta = await this.getChannelMeta(contentId, false)
151 | if (meta.dRights) {
152 | let newBody = {
153 | contentID: contentId,
154 | contentType: '0x02',
155 | identityCookie,
156 | authorizationToken: meta.dRights.playToken,
157 | licenseChallenge: challenge
158 | }
159 | let options = { url: widevineUrl, headers: headers, body: newBody, json: true }
160 | let licenseData = await rp.post(options)
161 | return Buffer.from(licenseData.licenseData[0], 'base64')
162 | } else {
163 | console.err('no license rights for', contentId)
164 | return Promise.resolve(null)
165 | }
166 | }
167 | }
168 |
169 | module.exports = DTVNowManager
170 |
--------------------------------------------------------------------------------
/src/services/dtv-now-router.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 |
4 | class DTVNowRouter {
5 | constructor (dtvNowManager, util) {
6 | this.dtvNowManager = dtvNowManager
7 | this.router = express.Router()
8 | this.util = util
9 | this.router.use(bodyParser.raw({ type: function (req) { return true } }))
10 |
11 | this.router.post('/widevine', this.processWidevine.bind(this))
12 | this.router.get('/login', this.login.bind(this))
13 | this.router.get('/schedule', this.getSchedule.bind(this))
14 | this.router.get('/meta', this.getChannelMeta.bind(this))
15 | this.router.use(this.routeError.bind(this))
16 | }
17 |
18 | processWidevine (req, res) {
19 | res.setHeader('Content-Type', 'application/octet-stream')
20 | this.util.r(req, res, () => this.dtvNowManager.processWidevine(req.body, req.query.contentId))
21 | }
22 |
23 | login (req, res) {
24 | this.util.r(req, res, () => this.dtvNowManager.login())
25 | }
26 |
27 | getSchedule (req, res) {
28 | this.util.r(req, res, () => this.dtvNowManager.getSchedule())
29 | }
30 |
31 | getChannelMeta (req, res) {
32 | this.util.r(req, res, () => this.dtvNowManager.getChannelMeta(req.query.ccid))
33 | }
34 |
35 | routeError (err, req, res, next) {
36 | this.util.routeError(err, req, res, next)
37 | }
38 | }
39 |
40 | module.exports = DTVNowRouter
41 |
--------------------------------------------------------------------------------
/src/services/live-manager.js:
--------------------------------------------------------------------------------
1 | //DEV PROD ENVIRONMENT AS TEST - PRED
2 | var rp = require('request-promise')
3 | const jar = rp.jar()
4 | const XmlJs = require('xml2js')
5 | const mpdParser = require('mpd-parser')
6 | const execPromise = require('child-process-promise').exec
7 | const url = require('url')
8 | const path = require('path')
9 | const XmlParser = new XmlJs.Parser()
10 | let downloadDir = `video/live-downloads`
11 |
12 | const username = process.env.LIVE_USER
13 | const password = process.env.LIVE_PASS
14 |
15 | class LiveManager {
16 | constructor (util, redisClient) {
17 | this.util = util
18 | this.redisClient = redisClient
19 | this.config = {}
20 | }
21 |
22 | login () {
23 | let loginOpts = {
24 | url: 'https://2001live.com/login',
25 | jar: jar
26 | }
27 | return rp.get(loginOpts).then(result => {
28 | let tokenReg = result.match(new RegExp(''))
29 | let options = {
30 | url: 'https://2001live.com/account/login',
31 | form: {
32 | _token: tokenReg[1],
33 | email: username,
34 | password: password,
35 | remember: 'on'
36 | },
37 | jar: jar,
38 | simple: false
39 | }
40 | return rp.post(options)
41 | })
42 | }
43 |
44 | getStream (id) {
45 | return this.login().then(() => {
46 | let options = {
47 | url: 'https://2001live.com/' + id,
48 | jar: jar
49 | }
50 | return rp.get(options).then(result => {
51 | let manifest = /dash: "(.*)",/.exec(result)[1]
52 | let widevine = /widevine: {([\s\S]*?)},/.exec(result)[1]
53 | let customData = /value: "(.*)"/.exec(widevine)[1]
54 | let licenseUrl = /LA_URL: "(.*)"/.exec(widevine)[1]
55 | return { manifest, customData, licenseUrl }
56 | })
57 | })
58 | }
59 |
60 | processWidevine (body) {
61 | return this.getStream('mainstage').then(res => {
62 | let options = {
63 | url: res.licenseUrl,
64 | body: body,
65 | headers: {
66 | customdata: res.customData
67 | },
68 | encoding: null
69 | }
70 | return rp.post(options)
71 | })
72 | }
73 |
74 | getSubStream (id) {
75 | return this.getStream(id).then(streamData => {
76 | return this.mpdRequest(streamData.manifest).then(xml => {
77 | return new Promise((resolve, reject) => {
78 | XmlParser.parseString(xml, function (err, result) {
79 | if (err) {
80 | reject(err)
81 | } else {
82 | console.log(result.MPD)
83 | resolve(result.MPD.Location[0])
84 | }
85 | })
86 | })
87 | })
88 | })
89 | }
90 |
91 | async mpdRequest (url) {
92 | let gif = url.replace(path.basename(url), 'image.gif')
93 | let redisGif = await this.redisClient.getAsync(gif)
94 | if (!redisGif) {
95 | let gifResp = await rp.get({ uri: gif, headers: { Referer: 'https://2001live.com' } })
96 | this.redisClient.setex(gif, 600, gifResp)
97 | }
98 | return rp.get(url)
99 | }
100 |
101 | mpdRefresher () {
102 | let self = this
103 | return async function processMpd (id) {
104 | id = id.split('-')[1]
105 | let mpd = await self.redisClient.getAsync(id)
106 | self.config[id] = self.config[id] || {}
107 | if (!mpd) {
108 | mpd = await self.getSubStream(id).then(manifest => {
109 | self.redisClient.set(id, manifest)
110 | return manifest
111 | })
112 | } else {
113 | mpd = mpd.toString()
114 | }
115 | return self.mpdRequest(mpd).then(manifest => {
116 | let parsedManifest = mpdParser.parse(manifest, mpd)
117 | let playlists = self.util.getMaxPlaylists(parsedManifest, 'engmp4a.40.2')
118 | let pssh = playlists.maxVideo.contentProtection['com.widevine.alpha'].psshNormal
119 | let currentTime = new Date()
120 | self.config[id].videoStartTime = self.config[id].videoStartTime || new Date(currentTime)
121 | self.config[id].audioStartTime = self.config[id].audioStartTime || new Date(currentTime)
122 | self.config[id].init = self.config[id].init || {}
123 | for (let vidSeg of playlists.maxVideo.segments) {
124 | vidSeg.resolvedUri = url.resolve(mpd, vidSeg.uri)
125 | self.config[id].init.video = url.resolve(mpd, vidSeg.map.uri)
126 | self.config[id].segments = self.config[id].segments || {}
127 | vidSeg.startTime = new Date(self.config[id].videoStartTime)
128 | self.config[id].videoStartTime.setSeconds(self.config[id].videoStartTime.getSeconds() + vidSeg.duration)
129 | setTimeout(() => {
130 | let outFile = self.util.randomFileName('.m4s')
131 | execPromise(`aria2c ${vidSeg.resolvedUri} --out=${outFile} --dir ${downloadDir}`).then(() => {
132 | self.config[id].segments[pssh] = self.config[id].segments[pssh] || []
133 | self.config[id].segments[pssh].push(path.resolve(`${downloadDir}/${outFile}`))
134 | })
135 | }, vidSeg.startTime.getTime() - new Date().getTime())
136 | }
137 | for (let audSeg of playlists.maxAudio.segments) {
138 | audSeg.resolvedUri = url.resolve(mpd, audSeg.uri)
139 | self.config[id].init.audio = url.resolve(mpd, audSeg.map.uri)
140 | self.config[id].audio = self.config[id].audio || {}
141 | audSeg.startTime = new Date(self.config[id].audioStartTime)
142 | self.config[id].audioStartTime.setSeconds(self.config[id].audioStartTime.getSeconds() + audSeg.duration)
143 | setTimeout(() => {
144 | let outFile = self.util.randomFileName('.m4s')
145 | execPromise(`aria2c ${audSeg.resolvedUri} --out=${outFile} --dir ${downloadDir}`).then(() => {
146 | self.config[id].audio[pssh] = self.config[id].audio[pssh] || []
147 | self.config[id].audio[pssh].push(path.resolve(`${downloadDir}/${outFile}`))
148 | })
149 | }, audSeg.startTime.getTime() - new Date().getTime())
150 | }
151 | return true
152 | })
153 | }
154 | }
155 |
156 | getSegments () {
157 | let self = this
158 | return async function processSegments (id) {
159 | id = id.split('-')[1]
160 | let downloadDir = `video/${self.util.randomFileName()}`
161 | let outFile = self.util.randomFileName('.m4s')
162 | let initVideo = await execPromise(`aria2c ${self.config[id].init.video} --out=${outFile} --dir ${downloadDir}`).then(() => {
163 | return path.resolve(`${downloadDir}/${outFile}`)
164 | })
165 | outFile = self.util.randomFileName('.m4s')
166 | let initAudio = await execPromise(`aria2c ${self.config[id].init.audio} --out=${outFile} --dir ${downloadDir}`).then(() => {
167 | return path.resolve(`${downloadDir}/${outFile}`)
168 | })
169 | for (let pssh in self.config[id].segments) {
170 | self.config[id].segments[pssh] = [initVideo].concat(self.config[id].segments[pssh])
171 | }
172 | for (let pssh in self.config[id].audio) {
173 | self.config[id].audio[pssh] = [initAudio].concat(self.config[id].audio[pssh])
174 | }
175 | let segments = self.config[id].segments
176 | let audio = self.config[id].audio
177 |
178 | self.config[id].audio = {}
179 | self.config[id].segments = {}
180 | return Promise.resolve({ segments, audio })
181 | }
182 | }
183 |
184 | async getExoSchedule (host) {
185 | let ids = ['mainstage', 'dressingRoom']
186 | let items = [{ name: '2001live', samples: [] }]
187 | let promises = []
188 | for (let id of ids) {
189 | let res = await this.getStream(id)
190 | items[0].samples.push({
191 | name: id,
192 | uri: res.manifest,
193 | drm_scheme: 'widevine',
194 | drm_license_url: `${host}/live/widevine`
195 | })
196 | }
197 | return Promise.all(promises).then(() => {
198 | return items
199 | })
200 | }
201 |
202 | async getSchedule (host) {
203 | let ids = ['mainstage', 'dressingRoom']
204 | let items = { items: [] }
205 | for (let id of ids) {
206 | let res = await this.getStream(id)
207 | items.items.push({ channel: {
208 | title: Buffer.from(id).toString('base64'),
209 | stream_url: res.manifest,
210 | desc_image: null,
211 | drm: {
212 | protocol: 'mpd',
213 | type: 'com.widevine.alpha',
214 | license_url: `${host}/live/widevine`
215 | },
216 | description: null,
217 | category_id: null
218 | } })
219 | }
220 | let builder = new XmlJs.Builder({ cdata: true })
221 | return builder.buildObject(items)
222 | }
223 | }
224 |
225 | module.exports = LiveManager
226 |
--------------------------------------------------------------------------------
/src/services/live-router.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 | let root = 'live'
4 |
5 | class LiveRouter {
6 | constructor (liveManager, util, videoManager) {
7 | this.router = express.Router()
8 | this.util = util
9 | this.liveManager = liveManager
10 | this.videoManager = videoManager
11 | this.router.use(bodyParser.raw({ type: function (req) { return true } }))
12 | this.router.get('/stream', this.stream.bind(this))
13 | this.router.post('/widevine', this.processWidevine.bind(this))
14 | this.router.get('/schedule', this.schedule.bind(this))
15 | this.router.get('/build', this.build.bind(this))
16 | this.router.use(this.routeError.bind(this))
17 | }
18 |
19 | stream (req, res) {
20 | this.util.r(req, res, () => this.liveManager.getStream(req.query.id))
21 | }
22 |
23 | build (req, res) {
24 | this.util.r(req, res, () => this.videoManager.startBuild(`${root}-${req.query.channelId}`, 'http://localhost:3001/live/widevine', this.liveManager.getSegments(), this.liveManager.mpdRefresher(), true))
25 | }
26 |
27 | processWidevine (req, res) {
28 | this.util.r(req, res, () => this.liveManager.processWidevine(req.body))
29 | }
30 |
31 | schedule (req, res) {
32 | this.util.r(req, res, () => this.liveManager.getSchedule(this.util.getReqHost(req)))
33 | }
34 |
35 | routeError (err, req, res, next) {
36 | this.util.routeError(err, req, res, next)
37 | }
38 | }
39 |
40 | module.exports = LiveRouter
41 |
--------------------------------------------------------------------------------
/src/services/rap-manager.js:
--------------------------------------------------------------------------------
1 | var rp = require('request-promise')
2 | const deviceIds = [
3 | '389ab513-0936-4be6-9eeb-6f8ac7f48d6b'
4 | ]
5 | const username = process.env.RAP_USER
6 | const password = process.env.RAP_PASS
7 | const proxy = undefined
8 | const querystring = require('query-string')
9 |
10 | class RAPManager {
11 | constructor (redisClient, util) {
12 | this.util = util
13 | this.redisClient = redisClient
14 | }
15 |
16 | async login (deviceId) {
17 | let cacheId = `rap_login_${deviceId}`
18 | let loginData = await this.redisClient.getAsync(cacheId)
19 | if (loginData) {
20 | return JSON.parse(loginData)
21 | }
22 | loginData = await rp.post('https://raptv.umsgw.quickplay.com/qp/login?lang=en', { body: { deviceId, username, password }, proxy, json: true })
23 | let tToken = loginData.data.tToken
24 | let mac = loginData.data.can[0].stb[0].mac
25 |
26 | let options = {
27 | qs: {
28 | deviceId,
29 | 'stb-mac': mac,
30 | lang: 'en'
31 | },
32 | headers: {
33 | 'x-authorization': tToken
34 | },
35 | proxy,
36 | json: true
37 | }
38 | let entitlementData = await rp.get('https://raptv.umsgw.quickplay.com/qp/entitlements', options)
39 | this.redisClient.setex(cacheId, 80000, JSON.stringify(entitlementData))
40 |
41 | return entitlementData
42 | }
43 |
44 | async getChannels () {
45 | let deviceId = deviceIds[Math.floor(Math.random() * deviceIds.length)]
46 | let entitlementData = await this.login(deviceId)
47 | let newChannels = []
48 | for (let channel of entitlementData.data.entitlements) {
49 | if (!newChannels.some(x => x.playBackId === channel.playBackId)) {
50 | let url = `http://localhost:3001/video/master.m3u8?${querystring.stringify({ mpd: `http://localhost:3001/rap/${channel.playBackId}.mpd`, licenseUrl: `http://localhost:3001/rap/widevine?playbackId=${channel.playBackId}` })}`
51 | newChannels.push({ url, name: channel.name, playBackId: channel.playBackId })
52 | }
53 | }
54 | return newChannels
55 | }
56 |
57 | async getStreamTokens (uat, deviceId) {
58 | let form = {
59 | 'action': '1',
60 | 'render': 'json',
61 | 'roamingCheck': 'false',
62 | 'deviceName': 'webClient',
63 | 'locale': 'en_CA',
64 | 'network': 'wifi',
65 | 'uniqueId': deviceId,
66 | 'UAT': uat,
67 | 'appId': '6013',
68 | 'apiVersion': '6',
69 | 'apiRevision': '0',
70 | 'carrierId': '5',
71 | 'clientBuild': '0002',
72 | 'clientVersion': '2.0',
73 | 'bitrate': '1000000',
74 | 'iu': '/7326/en.raptv.web/',
75 | 'lang': 'en',
76 | 'sz': '640x360'
77 | }
78 | let body = querystring.stringify(form)
79 | let resp = await rp.post('https://raptv.vstb.quickplay.com/vstb/app', { body, proxy, headers: { 'content-type': 'text/plain;charset=UTF-8' } })
80 | return JSON.parse(resp)
81 | }
82 |
83 | async getStreamData (playbackId) {
84 | let cacheId = `rap_stream_${playbackId}`
85 | let cache = await this.redisClient.getAsync(cacheId)
86 | if (cache) {
87 | return JSON.parse(cache)
88 | }
89 | let deviceId = deviceIds[Math.floor(Math.random() * deviceIds.length)]
90 | let loginData = await this.login(deviceId)
91 | let streamTokenData = await this.getStreamTokens(loginData.data.ovat, deviceId)
92 | let form = {
93 | 'action': '101',
94 | 'render': 'json',
95 | 'roamingCheck': 'false',
96 | 'deviceName': 'webClient',
97 | 'locale': 'en_CA',
98 | 'delivery': '5',
99 | 'UAT': loginData.data.ovat,
100 | 'mak': streamTokenData.mak,
101 | 'network': 'wifi',
102 | 'drmToken': streamTokenData.mak,
103 | 'uniqueId': deviceId,
104 | 'subscriberId': deviceId,
105 | 'contentId': playbackId,
106 | 'contentTypeId': '4',
107 | 'preferredMediaPkgs': 'DASH',
108 | 'preferredDRM': '6:2.0,0:',
109 | 'appId': '6013',
110 | 'apiVersion': '6',
111 | 'apiRevision': '0',
112 | 'carrierId': '5',
113 | 'clientBuild': '0002',
114 | 'clientVersion': '2.0',
115 | 'bitrate': '1000000',
116 | 'iu': '/7326/en.raptv.web/',
117 | 'lang': 'en',
118 | 'sz': '640x360',
119 | 'url': `https://www.rogersanyplacetv.com/live/${playbackId}/full`
120 | }
121 | let body = querystring.stringify(form)
122 | let resp = await rp.post('https://raptv.vstb.quickplay.com/vstb/app', { body, proxy, headers: { 'content-type': 'text/plain;charset=UTF-8' } })
123 | resp = JSON.parse(resp)
124 | this.redisClient.setex(cacheId, 500, JSON.stringify(resp))
125 | return resp
126 | }
127 |
128 | async getStreamUrl (playbackId) {
129 | let streamData = await this.getStreamData(playbackId)
130 | let rightsObject = this.util.decryptRightsObject(streamData.rightsObject)
131 | return Promise.resolve({ url: rightsObject.contentUrl })
132 | }
133 |
134 | async processWidevine (body, playbackId) {
135 | let streamData = await this.getStreamData(playbackId)
136 | let rightsObject = this.util.decryptRightsObject(streamData.rightsObject)
137 | if (Object.keys(body).length === 0) body = '\x08\x04'
138 | let widevineUrl = rightsObject.drmAttributes.widevineLicenseProxyAddr + rightsObject.drmAttributes.widevineLicenseQParams
139 | let options = { uri: widevineUrl, proxy, body: body, encoding: null }
140 | return rp.post(options)
141 | }
142 | }
143 |
144 | module.exports = RAPManager
145 |
--------------------------------------------------------------------------------
/src/services/rap-router.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 |
4 | class RAPRouter {
5 | constructor (rapManager, util) {
6 | this.rapManager = rapManager
7 | this.router = express.Router()
8 | this.util = util
9 | this.router.use(bodyParser.raw({ type: function (req) { return true } }))
10 |
11 | this.router.get('/:id.mpd', this.getStream.bind(this))
12 | this.router.post('/widevine', this.processWidevine.bind(this))
13 | this.router.get('/channels', this.channels.bind(this))
14 |
15 | this.router.use(this.routeError.bind(this))
16 | }
17 |
18 | processWidevine (req, res) {
19 | res.setHeader('Content-Type', 'application/octet-stream')
20 | this.util.r(req, res, () => this.rapManager.processWidevine(req.body, req.query.playbackId))
21 | }
22 |
23 | channels (req, res) {
24 | this.util.r(req, res, () => this.rapManager.getChannels())
25 | }
26 |
27 | getStream (req, res) {
28 | this.rapManager.getStreamUrl(req.params.id).then(streamData => {
29 | res.redirect(streamData.url)
30 | })
31 | }
32 |
33 | routeError (err, req, res, next) {
34 | this.util.routeError(err, req, res, next)
35 | }
36 | }
37 |
38 | module.exports = RAPRouter
39 |
--------------------------------------------------------------------------------
/src/services/sling-router.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 | let root = 'sling'
4 | class SlingRouter {
5 | constructor (slingManager, util, videoManager) {
6 | this.slingManager = slingManager
7 | this.videoManager = videoManager
8 | this.router = express.Router()
9 | this.util = util
10 | this.router.use(bodyParser.raw({ type: function (req) { return true } }))
11 |
12 | this.router.post('/yGsZQrFlUn', this.processWidevine.bind(this))
13 | this.router.get('/:channelId.mpd', this.getStream.bind(this))
14 | this.router.get('/schedule', this.getSchedule.bind(this))
15 | this.router.get('/schedule.json', this.getRawSchedule.bind(this))
16 | this.router.get('/scheduleStatus.json', this.getScheduleStatus.bind(this))
17 | this.router.get('/segments', this.getSegments.bind(this))
18 | this.router.get('/build', this.build.bind(this))
19 | this.router.get('/stop', this.stop.bind(this))
20 | this.router.get('/findTestCases', this.findTestCases.bind(this))
21 | // this.router.get('/new.m3u8', this.rewriteM3u8.bind(this))
22 |
23 | this.router.use(this.routeError.bind(this))
24 | }
25 |
26 | rewriteM3u8 (req, res) {
27 | res.setHeader('content-type', 'application/x-mpegURL')
28 | this.util.r(req, res, () => this.slingManager.rewriteM3u8(req.query.channelId))
29 | }
30 |
31 | getSegments (req, res) {
32 | this.util.r(req, res, () => this.slingManager.getSegments(req.query.channelId))
33 | }
34 |
35 | findTestCases (req, res) {
36 | this.slingManager.findTestCases()
37 | }
38 |
39 | build (req, res) {
40 | this.util.r(req, res, () => this.videoManager.startBuild(`${root}-${req.query.channelId}`, 'http://localhost:3001/sling/yGsZQrFlUn', this.slingManager.getSegmentsv3()))
41 | }
42 |
43 | stop (req, res) {
44 | this.util.r(req, res, () => this.videoManager.stop(`${root}-${req.query.channelId}`))
45 | }
46 |
47 | processWidevine (req, res) {
48 | this.util.r(req, res, () => this.slingManager.processWidevine(req.body, req.query.channelId))
49 | }
50 |
51 | getStream (req, res) {
52 | res.setHeader('Content-Type', 'application/dash+xml')
53 | this.util.r(req, res, () => this.slingManager.getStream(req.params.channelId, req.query.rewrite !== 'false'))
54 | }
55 |
56 | getSchedule (req, res) {
57 | res.setHeader('Content-Type', 'application/xml')
58 | let host = this.util.getReqHost(req)
59 | this.util.r(req, res, () => this.slingManager.getSchedule(host))
60 | }
61 |
62 | getScheduleStatus (req, res) {
63 | this.util.r(req, res, () => this.slingManager.getScheduleStatus())
64 | }
65 |
66 | getRawSchedule (req, res) {
67 | this.util.r(req, res, () => this.slingManager.getRawSchedule())
68 | }
69 |
70 | routeError (err, req, res, next) {
71 | this.util.routeError(err, req, res, next)
72 | }
73 | }
74 |
75 | module.exports = SlingRouter
76 |
--------------------------------------------------------------------------------
/src/services/tatasky-manager.js:
--------------------------------------------------------------------------------
1 | var rp = require('request-promise')
2 | var qs = require('query-string')
3 | let scheduleUrl = 'https://kong-tatasky.videoready.tv/portal-search/pub/api/v1/channels?languageFilters=&genreFilters=&limit=5000&offset=0&ott=true'
4 | let ip = ''
5 |
6 | class TataSkyManager {
7 | constructor (util, redisClient, videoManager) {
8 | this.util = util
9 | this.videoManager = videoManager
10 | this.redisClient = redisClient
11 | this.config = {}
12 | }
13 |
14 | async getStream (id) {
15 | let streamDataUrl = `https://kong-tatasky.videoready.tv/content-detail/pub/api/v2/channels/${id}?platform=WEB`
16 | let streamData = await rp.get(streamDataUrl, { json: true })
17 | return streamData.data.detail
18 | }
19 |
20 | async processWidevine (body) {
21 | let loginData = await this.login()
22 | let options = {
23 | url: 'https://tatasky.live.ott.irdeto.com/Widevine/getlicense',
24 | qs: {
25 | CrmId: 'tatasky',
26 | AccountId: '',
27 | ContentId: '',
28 | SessionId: loginData.rrmSessionInfo.sessionId,
29 | Ticket: loginData.rrmSessionInfo.ticket
30 | },
31 | body: body,
32 | headers: {
33 | 'X-Forwarded-For': ip
34 | },
35 | encoding: null
36 | }
37 | return rp.post(options)
38 | }
39 |
40 | async login () {
41 | let cacheId = 'tatasky_login'
42 | let cache = await this.redisClient.getAsync(cacheId)
43 | if (cache) return JSON.parse(cache)
44 | let options = {
45 | body: {
46 | sid: process.env.TATASKY_USER_SID,
47 | pwd: process.env.TATASKY_PASS
48 | },
49 | json: true
50 | }
51 | let loginData = await rp.post('https://kong-tatasky.videoready.tv/rest-api/pub/api/v1/pwdLogin', options)
52 | this.redisClient.setex(cacheId, 86400, JSON.stringify(loginData.data))
53 | return loginData.data
54 | }
55 |
56 | async schedule () {
57 | let loginData = await this.login()
58 | let entitlements = loginData.userDetails.entitlements.map(x => x.pkgId)
59 | let schedule = await rp.get(scheduleUrl, { json: true })
60 | let channels = []
61 | let headers = { 'x-forwarded-for': ip }
62 | for (let channel of schedule.data.list) {
63 | if (channel.entitlements.some(r => entitlements.includes(r))) {
64 | channels.push(this.getStream(channel.id).then(channelData => {
65 | let url = `http://localhost:3001/video/master.m3u8?${qs.stringify({ mpd: channelData.dashWidewinePlayUrl, licenseUrl: `http://localhost:3001/tatasky/widevine`, headers: JSON.stringify(headers) })}`
66 | return { title: channel.title, url }
67 | }))
68 | }
69 | }
70 | return Promise.all(channels)
71 | }
72 | }
73 |
74 | module.exports = TataSkyManager
75 |
--------------------------------------------------------------------------------
/src/services/tatasky-router.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 |
4 | class TataSkyRouter {
5 | constructor (tataSkyManager, util, videoManager) {
6 | this.router = express.Router()
7 | this.util = util
8 | this.tataSkyManager = tataSkyManager
9 | this.videoManager = videoManager
10 | this.router.use(bodyParser.raw({ type: function (req) { return true } }))
11 | this.router.post('/widevine', this.processWidevine.bind(this))
12 | this.router.get('/schedule', this.schedule.bind(this))
13 | this.router.use(this.routeError.bind(this))
14 | }
15 |
16 | processWidevine (req, res) {
17 | this.util.r(req, res, () => this.tataSkyManager.processWidevine(req.body))
18 | }
19 |
20 | schedule (req, res) {
21 | this.util.r(req, res, () => this.tataSkyManager.schedule())
22 | }
23 |
24 | routeError (err, req, res, next) {
25 | this.util.routeError(err, req, res, next)
26 | }
27 | }
28 |
29 | module.exports = TataSkyRouter
30 |
--------------------------------------------------------------------------------
/src/services/tsn-manager.js:
--------------------------------------------------------------------------------
1 | var rp = require('request-promise')
2 | const fairplayCertUrl = 'https://license.9c9media.ca/fairplay/cert'
3 | const fairplayCkcUrl = 'https://license.9c9media.ca/fairplay/ckc'
4 | const widevineUrl = 'https://license.9c9media.ca/widevine'
5 | //const playReadyUrl = 'https://license.9c9media.ca/playready'
6 |
7 | class TSNManager {
8 | getFairplayCert () {
9 | let options = { url: fairplayCertUrl, encoding: null }
10 | return rp.get(options).then(body => {
11 | return Buffer.from(body).toString('base64')
12 | })
13 | }
14 |
15 | processFairplayCert (body) {
16 | let options = { url: fairplayCkcUrl, body: body, encoding: null }
17 | return rp.post(options).then(body => {
18 | return Buffer.from(body).toString('base64')
19 | })
20 | }
21 |
22 | processWidevine (body) {
23 | if (Object.keys(body).length === 0) body = '\x08\x04'
24 | let options = { url: widevineUrl, body: body, encoding: null }
25 | return rp.post(options)
26 | }
27 |
28 | processPlayReady (body) {
29 | let options = { url: widevineUrl, body: body, encoding: null }
30 | return rp.post(options)
31 | }
32 | }
33 |
34 | module.exports = TSNManager
35 |
--------------------------------------------------------------------------------
/src/services/tsn-router.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 |
4 | class TSNRouter {
5 | constructor (tsnManager, util) {
6 | this.tsnManager = tsnManager
7 | this.router = express.Router()
8 | this.util = util
9 | this.router.use(bodyParser.raw({ type: function (req) { return true } }))
10 |
11 | this.router.get('/bKIvEvxhlH', this.getFairplayCert.bind(this))
12 | this.router.post('/g1Ilryrq0i', this.processFairplayCert.bind(this))
13 | this.router.post('/yGsZQrFlUn', this.processWidevine.bind(this))
14 | this.router.post('/6Wn029orzF', this.processPlayReady.bind(this))
15 |
16 | this.router.use(this.routeError.bind(this))
17 | }
18 |
19 | getFairplayCert (req, res) {
20 | res.setHeader('Content-Type', 'application/octet-stream')
21 | this.util.r(req, res, () => this.tsnManager.getFairplayCert())
22 | }
23 |
24 | processFairplayCert (req, res) {
25 | res.setHeader('Content-Type', 'application/octet-stream')
26 | this.util.r(req, res, () => this.tsnManager.processFairplayCert(req.body))
27 | }
28 |
29 | processWidevine (req, res) {
30 | res.setHeader('Content-Type', 'application/octet-stream')
31 | this.util.r(req, res, () => this.tsnManager.processWidevine(req.body))
32 | }
33 |
34 | processPlayReady (req, res) {
35 | res.setHeader('Content-Type', 'application/octet-stream')
36 | this.util.r(req, res, () => this.tsnManager.processPlayReady(req.body))
37 | }
38 |
39 | routeError (err, req, res, next) {
40 | this.util.routeError(err, req, res, next)
41 | }
42 | }
43 |
44 | module.exports = TSNRouter
45 |
--------------------------------------------------------------------------------
/src/utils/util.js:
--------------------------------------------------------------------------------
1 | const HLS = require('hls-parser')
2 | const request = require('request-promise')
3 | const readHLS = require('m3u8-reader')
4 | const writeHLS = require('m3u8-write')
5 | const url = require('url')
6 | const crypto = require('crypto')
7 | const CryptoJS = require('crypto-js')
8 |
9 | class Util {
10 | constructor (origin) {
11 | this.origin = origin
12 | }
13 | postError (logData, extra = null) {
14 | let msg = 'module: ' + this.origin
15 | if (extra) {
16 | msg += ' (' + extra + ')'
17 | }
18 | msg = msg + ' error: '
19 | console.error(msg, logData)
20 | }
21 |
22 | getReqHost (req) {
23 | let host = req.protocol + '://' + req.hostname
24 | if (process.env.PORT) {
25 | host = host + ':' + process.env.PORT
26 | }
27 | return host
28 | }
29 |
30 | postInfo (logData) {
31 | console.info('module: ' + this.origin + ' info: ', logData)
32 | }
33 | routeError (err, req, res, next) {
34 | this.postError(err)
35 | this.sendError(req, res, 500, err)
36 | }
37 | createError (code, err = null, customclientErrorMessage = null) {
38 | let clientErrorMessage
39 | if (customclientErrorMessage) {
40 | clientErrorMessage = customclientErrorMessage
41 | } else {
42 | switch (code) {
43 | case 500:
44 | clientErrorMessage = 'Unexpected server error'
45 | break
46 | case 404:
47 | clientErrorMessage = 'object not found'
48 | break
49 | case 400:
50 | clientErrorMessage = 'bad request'
51 | break
52 | }
53 | }
54 | let obj = { clientErrorMessage: clientErrorMessage, statusCode: code }
55 | if (err) {
56 | obj.devErrorMessage = err.message
57 | obj.stack = err.stack
58 | }
59 | return obj
60 | }
61 |
62 | badInputError (msg = 'Bad input', code = 400) {
63 | let err = new Error(msg)
64 | err.code = code
65 | err.customClientErrorMessage = msg
66 | return err
67 | }
68 |
69 | sendError (req, res, code, err = null, customClientErrorMessage = null) {
70 | if (err && err.code) {
71 | code = err.code
72 | }
73 | if (err && err.customClientErrorMessage) {
74 | customClientErrorMessage = err.customClientErrorMessage
75 | }
76 | let error = this.createError(code, err, customClientErrorMessage)
77 |
78 | let extra = null
79 | if (req) {
80 | extra = ''
81 | if (req.user && Object.keys(req.user).length > 0) {
82 | extra = extra + JSON.stringify(req.user)
83 | }
84 | if (req.body) {
85 | Object.keys(req.body).forEach(key => {
86 | if (key !== 'password') {
87 | extra = extra + ' ' + key + ':' + JSON.stringify(req.body[key])
88 | } else {
89 | extra = extra + ' password:XXXXXXX'
90 | }
91 | })
92 | }
93 | if (req.params && Object.keys(req.params).length > 0) {
94 | extra = extra + ' ' + JSON.stringify(req.params)
95 | }
96 | }
97 | this.postError(error, extra)
98 | res.status(code).json(error)
99 | }
100 | // Generic implementation for function that expects results
101 | r (req, res, funcResult) {
102 | try {
103 | funcResult()
104 | .then(obj => {
105 | if (obj) {
106 | if (typeof obj === 'object' && !Buffer.isBuffer(obj)) res.json(obj)
107 | else res.send(obj)
108 | } else {
109 | this.sendError(req, res, 404)
110 | }
111 | }, err => {
112 | this.sendError(req, res, 500, err)
113 | })
114 | } catch (err) {
115 | this.sendError(req, res, 500, err)
116 | }
117 | }
118 | // Generic implementation for function that doesn't expect results
119 | nr (req, res, funcResult) {
120 | try {
121 | funcResult()
122 | .then(obj => {
123 | if (obj) {
124 | res.sendStatus(204)
125 | } else {
126 | this.sendError(req, res, 404)
127 | }
128 | }, err => {
129 | this.sendError(req, res, 500, err)
130 | })
131 | } catch (err) {
132 | this.sendError(req, res, 500, err)
133 | }
134 | }
135 |
136 | makeRequest (options) {
137 | return request(options)
138 | }
139 |
140 | parseHLS (body, hlsUrl = null, cacheId = null, client = null) {
141 | let playlist = readHLS(body)
142 | for (let i in playlist) {
143 | // rewrite key files
144 | if (playlist[i] && playlist[i].KEY && playlist[i].KEY.URI) {
145 | let encodedKey = Buffer.from(playlist[i].KEY.URI).toString('base64')
146 | playlist[i].KEY.URI = `../key?key=${encodedKey}`
147 | }
148 | // rewrite variants and segments
149 | if (typeof playlist[i] === 'string') {
150 | // add host
151 | if (hlsUrl) {
152 | playlist[i] = url.resolve(hlsUrl, playlist[i])
153 | }
154 | // cache variant and change host to local
155 | if (cacheId) {
156 | let lastSegment = playlist[i - 1]
157 | if (lastSegment && lastSegment['STREAM-INF'] && lastSegment['STREAM-INF'].BANDWIDTH) {
158 | let bandwidth = lastSegment['STREAM-INF'].BANDWIDTH
159 | if (client) client.setex(`${cacheId}-${bandwidth}`, 43200, playlist[i])
160 | playlist[i] = `${cacheId}/${bandwidth}.m3u8`
161 | }
162 | }
163 | }
164 | }
165 | return writeHLS(playlist)
166 | }
167 |
168 | randomFileName (ext = '') {
169 | return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) + ext
170 | }
171 |
172 | findLastIndex (array, searchKey, searchValue) {
173 | let index = array.slice().reverse().findIndex(x => x[searchKey] === searchValue)
174 | let count = array.length - 1
175 | let finalIndex = index >= 0 ? count - index : index
176 | return finalIndex
177 | }
178 |
179 | sleep (ms) {
180 | return new Promise(resolve => setTimeout(resolve, ms))
181 | }
182 |
183 | getMD5 (string) {
184 | return crypto.createHash('md5').update(string).digest('hex')
185 | }
186 |
187 | decryptRightsObject (rights) {
188 | let key = CryptoJS.enc.Hex.parse('')
189 | let iv = CryptoJS.enc.Hex.parse('')
190 | let data = Buffer.from(rights, 'hex').toString('base64')
191 | let decrypted = CryptoJS.AES.decrypt({ ciphertext: CryptoJS.enc.Base64.parse(data), salt: '' }, key, { iv })
192 | return JSON.parse(Buffer.from(decrypted.toString(), 'hex').toString('utf8'))
193 | }
194 |
195 | getMaxPlaylists (parsedManifest, aCodec = null) {
196 | aCodec = aCodec || Object.keys(parsedManifest.mediaGroups.AUDIO.audio)[0]
197 | let audioPlaylists = parsedManifest.mediaGroups.AUDIO.audio[aCodec].playlists
198 | let maxB = 0
199 | let maxVideo = null
200 | for (let playlist of parsedManifest.playlists) {
201 | if (playlist.attributes.BANDWIDTH > maxB) {
202 | maxB = playlist.attributes.BANDWIDTH
203 | maxVideo = playlist
204 | }
205 | }
206 | let maxAudioB = 0
207 | let maxAudio = null
208 | for (let playlist of audioPlaylists) {
209 | if (playlist.attributes.BANDWIDTH > maxAudioB) {
210 | maxAudioB = playlist.attributes.BANDWIDTH
211 | maxAudio = playlist
212 | }
213 | }
214 | return { maxAudio, maxVideo }
215 | }
216 |
217 | maxBandwidthUri (manifest, manifestUrl) {
218 | const playlist = HLS.parse(manifest)
219 | let maxBw = 0
220 | let uri = null
221 | for (let variant of playlist.variants) {
222 | if (variant.bandwidth > maxBw) {
223 | maxBw = variant.bandwidth
224 | uri = url.resolve(manifestUrl, variant.uri)
225 | }
226 | }
227 | return uri
228 | }
229 | }
230 |
231 | module.exports = Util
232 |
--------------------------------------------------------------------------------
/src/utils/video-router.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 |
4 | class VideoRouter {
5 | constructor (util, videoManager) {
6 | this.videoManager = videoManager
7 | this.router = express.Router()
8 | this.util = util
9 | this.router.use(bodyParser.raw({ type: function (req) { return true } }))
10 |
11 | this.router.get('/stop', this.stop.bind(this))
12 | this.router.get('/master.m3u8', this.mpdToHLSManifest.bind(this))
13 | this.router.get('/master2.m3u8', this.audioVideoM3u8.bind(this))
14 | this.router.get('/variant.m3u8', this.mpdToHLSVariant.bind(this))
15 | this.router.get('/downloader.m4s', this.downloader.bind(this))
16 | this.router.get('/decrypted.mpd', this.decryptMpd.bind(this))
17 | this.router.get('/getMpd', this.getMpd.bind(this))
18 |
19 | this.router.use(this.routeError.bind(this))
20 | }
21 |
22 | getMpd (req, res) {
23 | this.util.r(req, res, () => this.videoManager.testMpd())
24 | }
25 |
26 | audioVideoM3u8 (req, res) {
27 | res.setHeader('Content-Type', 'application/x-mpegURL')
28 | let bandwidthIndex = 0
29 | if (req.query.bandwidthIndex === 'false') {
30 | bandwidthIndex = req.query.bandwidthIndex
31 | } else {
32 | bandwidthIndex = parseInt(req.query.bandwidthIndex) || undefined
33 | }
34 | this.videoManager.audioVideoM3u8(req.query.mpd, req.query.licenseUrl, bandwidthIndex, req.query.checkDTS).then(m3u8Dir => {
35 | res.redirect(m3u8Dir.replace('public/', '/dev/'))
36 | })
37 | }
38 |
39 | mpdToHLSVariant (req, res) {
40 | res.setHeader('Content-Type', 'application/x-mpegURL')
41 | this.util.r(req, res, () => this.videoManager.mpdToHLSVariantV1(req.query.bandwidth, req.query.mpd, req.query.audio, req.query.licenseUrl, req.query.headers))
42 | }
43 |
44 | decryptMpd (req, res) {
45 | res.setHeader('Content-Type', 'application/dash+xml')
46 | this.util.r(req, res, () => this.videoManager.decryptMpd(req.query.mpd))
47 | }
48 |
49 | mpdToHLSManifest (req, res) {
50 | res.setHeader('Content-Type', 'application/x-mpegURL')
51 | let bandwidthIndex = 0
52 | if (req.query.bandwidthIndex === 'false') {
53 | bandwidthIndex = req.query.bandwidthIndex
54 | } else {
55 | bandwidthIndex = parseInt(req.query.bandwidthIndex) || undefined
56 | }
57 | this.util.r(req, res, () => this.videoManager.mpdToHLSManifestV1(req.query.mpd, req.query.licenseUrl, bandwidthIndex, req.query.headers))
58 | }
59 |
60 | downloader (req, res) {
61 | res.setHeader('Content-Type', 'application/octet-stream')
62 | this.videoManager.downloaderV1(req.query.init, req.query.key, req.query.url, req.query.audio, req.query.headers).then(segment => {
63 | if (typeof directory === 'string') res.redirect(segment.replace('public/', '/dev/'))
64 | else res.send(segment)
65 | })
66 | }
67 |
68 | stop (req, res) {
69 | this.util.r(req, res, () => this.videoManager.stop(req.query.id))
70 | }
71 |
72 | routeError (err, req, res, next) {
73 | this.util.routeError(err, req, res, next)
74 | }
75 | }
76 |
77 | module.exports = VideoRouter
78 |
--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | pm2 --name Universal-DRM start npm -- start
3 |
--------------------------------------------------------------------------------
/status.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | pm2 ps
--------------------------------------------------------------------------------
/stop.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | pm2 delete Universal-DRM
--------------------------------------------------------------------------------