diff --git a/LICENSE b/LICENSE
index eff80c228f..7fce21cf32 100644
--- a/LICENSE
+++ b/LICENSE
@@ -225,3 +225,35 @@ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----
+
+Contains code from https://github.com/Dash-Industry-Forum/dash.js
+(See lib/mss/mss_utils.js and lib/transmuxer/mss_transmuxer.js)
+
+dash.js BSD License Agreement
+
+Copyright 2015 Dash Industry Forum
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ - Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+ - Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ - Neither the name of the Dash Industry Forum nor the names of its
+ contributors may be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index fd058950ab..fbb2e1a35c 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
# ![Shaka Player](docs/shaka-player-logo.png)
Shaka Player is an open-source JavaScript library for adaptive media. It plays
-adaptive media formats (such as [DASH][] and [HLS][]) in a browser, without
-using plugins or Flash. Instead, Shaka Player uses the open web standards
-[MediaSource Extensions][] and [Encrypted Media Extensions][].
+adaptive media formats (such as [DASH][], [HLS][] and [MSS][]) in a browser,
+without using plugins or Flash. Instead, Shaka Player uses the open web
+standards [MediaSource Extensions][] and [Encrypted Media Extensions][].
Shaka Player also supports [offline storage and playback][] of media using
[IndexedDB][]. Content can be stored on any browser. Storage of licenses
@@ -18,6 +18,7 @@ For details on what's coming next, see our [development roadmap](roadmap.md).
[DASH]: http://dashif.org/
[HLS]: https://developer.apple.com/streaming/
+[MSS]: https://learn.microsoft.com/en-us/iis/media/smooth-streaming/smooth-streaming-transport-protocol
[MediaSource Extensions]: https://www.w3.org/TR/media-source/
[Encrypted Media Extensions]: https://www.w3.org/TR/encrypted-media/
[IndexedDB]: https://www.w3.org/TR/IndexedDB-2/
@@ -84,6 +85,7 @@ supported. This supports both DASH and HLS manifests.
|:----:|:-------------:|:---:|:---:|:-------------------:|
|DASH |**Y** |**Y**| - |**Y** |
|HLS |**Y** |**Y**|**Y**| - |
+|MSS |**Y** | - | - | - |
You can also create a [manifest parser plugin][] to support custom manifest
formats.
@@ -159,6 +161,20 @@ HLS features **not** supported:
[MPEG-5 Part2 LCEVC]: https://www.lcevc.org
+## MSS features
+
+MSS features supported:
+ - VOD
+ - AAC and H.264
+ - Encrypted content (PlayReady)
+ - TTML/DFXP
+ - Only supported with [codem-isoboxer][]
+
+MSS features **not** supported:
+ - Live
+
+[codem-isoboxer]: https://github.com/Dash-Industry-Forum/codem-isoboxer
+
## DRM support matrix
|Browser |Widevine |PlayReady|FairPlay |ClearKey⁶ |
@@ -196,6 +212,7 @@ NOTES:
|:--------:|:--------:|:-------:|:-------:|:--------:|
|DASH |**Y** |**Y** | - |**Y** |
|HLS |**Y** |**Y** |**Y** ¹ | - |
+|MSS | - |**Y** | - | - |
NOTES:
- ¹: By default, FairPlay is handled using Apple's native HLS player, when on
@@ -211,6 +228,7 @@ Shaka Player supports:
- Can parse "sidx" box for DASH's SegmentBase@indexRange and
SegmentTemplate@index
- Can find and parse "tfdt" box to find segment start time in HLS
+ - For MSS, [codem-isoboxer][] v0.3.7+ is required
- WebM
- Depends on browser support for the container via MediaSource
- Can parse [cueing data][] elements for DASH's SegmentBase@indexRange and
diff --git a/build/misspellings.txt b/build/misspellings.txt
index 926b5952d4..479628361f 100644
--- a/build/misspellings.txt
+++ b/build/misspellings.txt
@@ -17,7 +17,7 @@
r'(?i)mananger': 'manager',
r'(?i)mani?fe?st': 'manifest',
r'(?i)mil+isecond': 'millisecond',
- r'(?i)oc+ur+(?!ed|ing|ence)': 'occur',
+ r'(?i)\boc+ur+(?!ed|ing|ence)': 'occur',
r'(?i)oc+ur+(ed|ing|ence)': r'occurr\1',
r'(?i)oc+ur+ance': 'occurrence',
r'(?i)parrent': 'parent',
diff --git a/build/types/manifests b/build/types/manifests
index 29426067ac..5e5b0450a5 100644
--- a/build/types/manifests
+++ b/build/types/manifests
@@ -2,4 +2,5 @@
+@dash
+@hls
++@mss
+@offline
diff --git a/build/types/mss b/build/types/mss
new file mode 100644
index 0000000000..a9f330d2ec
--- /dev/null
+++ b/build/types/mss
@@ -0,0 +1,7 @@
+# The MSS manifest parser plugin.
+
++../../lib/mss/content_protection.js
++../../lib/mss/mss_parser.js
++../../lib/mss/mss_utils.js
+
++../../lib/transmuxer/mss_transmuxer.js
diff --git a/demo/common/assets.js b/demo/common/assets.js
index 9b52e56f33..35537d7372 100644
--- a/demo/common/assets.js
+++ b/demo/common/assets.js
@@ -141,6 +141,8 @@ shakaAssets.Feature = {
DASH: shakaDemo.MessageIds.DASH,
// Set if the asset is an HLS manifest.
HLS: shakaDemo.MessageIds.HLS,
+ // Set if the asset is an MSS manifest.
+ MSS: shakaDemo.MessageIds.MSS,
// Set if the asset has at least one image stream.
THUMBNAILS: shakaDemo.MessageIds.THUMBNAILS,
@@ -903,6 +905,14 @@ shakaAssets.testAssets = [
.addFeature(shakaAssets.Feature.MP4)
.addFeature(shakaAssets.Feature.LIVE)
.addFeature(shakaAssets.Feature.THUMBNAILS),
+ new ShakaDemoAssetInfo(
+ /* name= */ 'Microsoft Smooth Streaming',
+ /* iconUri= */ 'https://reference.dashif.org/dash.js/latest/samples/lib/img/mss-1.jpg',
+ /* manifestUri= */ 'https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest',
+ /* source= */ shakaAssets.Source.DASH_IF)
+ .addFeature(shakaAssets.Feature.MSS)
+ .addFeature(shakaAssets.Feature.HIGH_DEFINITION)
+ .addFeature(shakaAssets.Feature.MP4),
// End DASH-IF Assets }}}
// bitcodin assets {{{
diff --git a/demo/common/message_ids.js b/demo/common/message_ids.js
index 767382aad5..d9adce6065 100644
--- a/demo/common/message_ids.js
+++ b/demo/common/message_ids.js
@@ -21,6 +21,7 @@ shakaDemo.MessageIds = {
LIVE: 'DEMO_LIVE',
MP2TS: 'DEMO_MP2TS',
MP4: 'DEMO_MP4',
+ MSS: 'DEMO_MSS',
MULTIPLE_LANGUAGES: 'DEMO_MULTIPLE_LANGUAGES',
OFFLINE: 'DEMO_OFFLINE',
STORED: 'DEMO_STORED',
@@ -86,6 +87,7 @@ shakaDemo.MessageIds = {
UNSUPPORTED_NO_OFFLINE: 'DEMO_UNSUPPORTED_NO_OFFLINE',
UNSUPPORTED_NO_KEY_SUPPORT: 'DEMO_UNSUPPORTED_NO_KEY_SUPPORT',
UNSUPPORTED_NO_LICENSE_SUPPORT: 'DEMO_UNSUPPORTED_NO_LICENSE_SUPPORT',
+ UNSUPPORTED_NO_MSS_SUPPORT: 'DEMO_UNSUPPORTED_NO_MSS_SUPPORT',
/* Visualizer. */
VISUALIZER_AUTO_SCREENSHOT_TOGGLE: 'DEMO_VISUALIZER_AUTO_SCREENSHOT_TOGGLE',
VISUALIZER_BUTTON: 'DEMO_VISUALIZER_BUTTON',
@@ -242,6 +244,7 @@ shakaDemo.MessageIds = {
MIN_PIXELS: 'DEMO_MIN_PIXELS',
MIN_TOTAL_BYTES: 'DEMO_MIN_TOTAL_BYTES',
MIN_WIDTH: 'DEMO_MIN_WIDTH',
+ MSS_SEQUENCE_MODE: 'DEMO_MSS_SEQUENCE_MODE',
NETWORK_INFORMATION: 'DEMO_NETWORK_INFORMATION',
NUMBER_DECIMAL_WARNING: 'DEMO_NUMBER_DECIMAL_WARNING',
NUMBER_INTEGER_WARNING: 'DEMO_NUMBER_INTEGER_WARNING',
diff --git a/demo/config.js b/demo/config.js
index 7b0350c7c0..6fd343ce85 100644
--- a/demo/config.js
+++ b/demo/config.js
@@ -254,7 +254,9 @@ shakaDemo.Config = class {
.addBoolInput_(MessageIds.DISABLE_THUMBNAILS,
'manifest.disableThumbnails')
.addBoolInput_(MessageIds.SEGMENT_RELATIVE_VTT_TIMING,
- 'manifest.segmentRelativeVttTiming');
+ 'manifest.segmentRelativeVttTiming')
+ .addBoolInput_(MessageIds.MSS_SEQUENCE_MODE,
+ 'manifest.mss.sequenceMode');
this.addRetrySection_('manifest', MessageIds.MANIFEST_RETRY_SECTION_HEADER);
}
diff --git a/demo/index.html b/demo/index.html
index db4a3a342b..eb8c41a389 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -38,6 +38,8 @@
+
+
diff --git a/demo/locales/en.json b/demo/locales/en.json
index 63f6addcc3..667a14462d 100644
--- a/demo/locales/en.json
+++ b/demo/locales/en.json
@@ -168,6 +168,8 @@
"DEMO_MIN_WIDTH": "Min Width",
"DEMO_MP2TS": "MPEG-2 TS",
"DEMO_MP4": "MP4",
+ "DEMO_MSS": "MSS",
+ "DEMO_MSS_SEQUENCE_MODE": "Enable MSS sequence mode",
"DEMO_MULTIPLE_LANGUAGES": "Multiple languages",
"DEMO_NAME": "Name",
"DEMO_NAME_ERROR": "Must be a unique name.",
@@ -245,6 +247,7 @@
"DEMO_UNSUPPORTED_NO_HLS_SUPPORT": "Your browser does not support HLS manifests.",
"DEMO_UNSUPPORTED_NO_KEY_SUPPORT": "Your browser does not support the required key systems.",
"DEMO_UNSUPPORTED_NO_LICENSE_SUPPORT": "Your browser does not support offline licenses for the required key systems.",
+ "DEMO_UNSUPPORTED_NO_MSS_SUPPORT": "Your browser does not support MSS manifests.",
"DEMO_UNSUPPORTED_NO_OFFLINE": "Your browser does not support offline storage.",
"DEMO_UPDATE_EXPIRATION_TIME": "Update expiration time",
"DEMO_UPDATE_INTERVAL_SECONDS": "Update interval seconds",
diff --git a/demo/locales/source.json b/demo/locales/source.json
index aae51fd4ec..4ea488cca0 100644
--- a/demo/locales/source.json
+++ b/demo/locales/source.json
@@ -679,6 +679,14 @@
"description": "Text that describes an asset that uses the MP4 container.",
"message": "[JARGON:MP4]"
},
+ "DEMO_MSS": {
+ "description": "Text that describes an asset that is packaged in an MSS manifest.",
+ "message": "[PROPER_NAME:MSS]"
+ },
+ "DEMO_MSS_SEQUENCE_MODE": {
+ "description": "The name of a configuration value.",
+ "message": "Enable MSS sequence mode"
+ },
"DEMO_MULTIPLE_LANGUAGES": {
"description": "A tag that marks an asset as having multiple languages.",
"message": "Multiple languages"
@@ -960,29 +968,33 @@
"message": "This asset is disabled."
},
"DEMO_UNSUPPORTED_NO_DASH_SUPPORT": {
- "description": "An error message that shows why an asset cannot be stored offline: the browser cannot play MPEG-DASH (https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP) content.",
+ "description": "An error message that shows why an asset cannot be stored or played.",
"message": "Your browser does not support [PROPER_NAME:MPEG-DASH] manifests."
},
"DEMO_UNSUPPORTED_NO_DOWNLOAD": {
- "description": "An error message that shows why an asset cannot be stored offline: the asset cannot be downloaded by any browser.",
+ "description": "An error message that shows why an asset cannot be stored offline: the asset cannot be downloaded.",
"message": "This asset cannot be downloaded."
},
"DEMO_UNSUPPORTED_NO_FORMAT_SUPPORT": {
- "description": "An error message that shows why an asset cannot be stored offline: the browser cannot play that sort of video.",
+ "description": "An error message that shows why an asset cannot be stored or played.",
"message": "Your browser does not support the required video format."
},
"DEMO_UNSUPPORTED_NO_HLS_SUPPORT": {
- "description": "An error message that shows why an asset cannot be stored offline: the browser cannot play HLS (https://en.wikipedia.org/wiki/HTTP_Live_Streaming) content.",
+ "description": "An error message that shows why an asset cannot be stored or played.",
"message": "Your browser does not support [PROPER_NAME:HLS] manifests."
},
"DEMO_UNSUPPORTED_NO_KEY_SUPPORT": {
- "description": "An error message that shows why an asset cannot be stored offline: the browser does not support the asset's Digital Rights Management system.",
+ "description": "An error message that shows why an asset cannot be stored or played.",
"message": "Your browser does not support the required key systems."
},
"DEMO_UNSUPPORTED_NO_LICENSE_SUPPORT": {
"description": "An error message that shows why an asset cannot be stored offline: the browser cannot store protected content offline.",
"message": "Your browser does not support offline licenses for the required key systems."
},
+ "DEMO_UNSUPPORTED_NO_MSS_SUPPORT": {
+ "description": "An error message that shows why an asset cannot be stored or played.",
+ "message": "Your browser does not support [PROPER_NAME:MSS] manifests."
+ },
"DEMO_UNSUPPORTED_NO_OFFLINE": {
"description": "An error message that shows why an asset cannot be stored offline: the browser does not support storing things offline, in general.",
"message": "Your browser does not support offline storage."
diff --git a/demo/main.js b/demo/main.js
index e8fb3b87b1..79f69ccff2 100644
--- a/demo/main.js
+++ b/demo/main.js
@@ -743,6 +743,10 @@ shakaDemo.Main = class {
!this.support_.manifest['m3u8']) {
return shakaDemo.MessageIds.UNSUPPORTED_NO_HLS_SUPPORT;
}
+ if (asset.features.includes(shakaAssets.Feature.MSS) &&
+ !this.support_.manifest['ism']) {
+ return shakaDemo.MessageIds.UNSUPPORTED_NO_MSS_SUPPORT;
+ }
// Does the asset contain a playable mime type?
const mimeTypes = [];
diff --git a/demo/search.js b/demo/search.js
index 0d5bcefd18..28215be439 100644
--- a/demo/search.js
+++ b/demo/search.js
@@ -359,7 +359,7 @@ shakaDemo.Search = class {
/* docLink= */ null);
this.makeSelectInput_(coreContainer,
shakaDemo.MessageIds.MANIFEST_SEARCH,
- [Feature.DASH, Feature.HLS], FEATURE);
+ [Feature.DASH, Feature.HLS, Feature.MSS], FEATURE);
this.makeSelectInput_(coreContainer,
shakaDemo.MessageIds.CONTAINER_SEARCH,
[Feature.MP4, Feature.MP2TS, Feature.WEBM, Feature.CONTAINERLESS],
diff --git a/demo/service_worker.js b/demo/service_worker.js
index 64dd61ba71..1d2f3b5cc9 100644
--- a/demo/service_worker.js
+++ b/demo/service_worker.js
@@ -95,6 +95,9 @@ const OPTIONAL_RESOURCES = [
// The mux.js transmuxing library for MPEG-2 TS and CEA support.
'../node_modules/mux.js/dist/mux.min.js',
+ // The codem-isoboxer library for MSS support.
+ '../node_modules/codem-isoboxer/dist/iso_boxer.min.js',
+
// The cast sender SDK.
'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js',
diff --git a/docs/tutorials/upgrade.md b/docs/tutorials/upgrade.md
index 5b2df4e4c3..c72421f4d4 100644
--- a/docs/tutorials/upgrade.md
+++ b/docs/tutorials/upgrade.md
@@ -96,3 +96,6 @@ application:
- Configuration changes:
- `streaming.forceTransmuxTS` has been renamed to `streaming.forceTransmux`
(deprecated in v4.3.0)
+
+ - Plugin changes:
+ - `Transmuxer` plugins now has two new parameters in `transmux()` method.
diff --git a/externs/isoboxer.js b/externs/isoboxer.js
new file mode 100644
index 0000000000..1a793f8835
--- /dev/null
+++ b/externs/isoboxer.js
@@ -0,0 +1,215 @@
+/*! @license
+ * Shaka Player
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * @fileoverview Externs for codem-isoboxer library.
+ * @externs
+ */
+
+
+/**
+ * @typedef {{
+ * Utils: !ISOBoxerUtils,
+ * parseBuffer: function(!(ArrayBuffer|ArrayBufferView)):!ISOBoxer,
+ * createBox: function(string, !ISOBoxer, boolean=):!ISOBoxer,
+ * createFullBox: function(string, !ISOBoxer, ?ISOBoxer=):!ISOBoxer,
+ * addBoxProcessor: function(string, function()):!ISOBoxer,
+ * createFile: function():!ISOBoxer,
+ * write: function():!ArrayBuffer,
+ * fetch: function(!ArrayBuffer):!ISOBoxer
+ * }}
+ * @property {!ISOBoxerUtils} Utils
+ * @property {function(!(ArrayBuffer|ArrayBufferView)):!ISOBoxer} parseBuffer
+ * @property {function(string, !ISOBoxer, boolean=):!ISOBoxer} createBox
+ * @property {function(string, !ISOBoxer, ?ISOBoxer=):!ISOBoxer} createFullBox
+ * @property {function(string, function()):!ISOBoxer} addBoxProcessor
+ * @property {function():!ISOBoxer} createFile
+ * @property {function():!ArrayBuffer} write
+ * @property {function(!ArrayBuffer):!ISOBoxer} fetch
+ * @const
+ */
+var ISOBoxer;
+
+
+/**
+ * @typedef {{
+ * appendBox: function(!ISOBoxer, !ISOBoxer):!ISOBox
+ * }}
+ * @const
+ */
+var ISOBoxerUtils;
+
+
+/**
+ * @typedef {{
+ * type: string,
+ * size: number,
+ * _parent: ISOBox,
+ * boxes: !Array.,
+ * entry: !Array.,
+ * version: !number,
+ * flags: !number,
+ * sample_count: !number,
+ * default_sample_info_size: !number,
+ * entry_count: !number,
+ * getLength: function():number,
+ * _procFullBox: function(),
+ * _procField: function(!string, !string, !number),
+ * _procFieldArray: function(!string, !number, !string, !number),
+ * _procEntries: function(!string, !number, !function(!ISOEntry)),
+ * _procEntryField: function(!ISOBox, !string, !string, !number),
+ * _procSubEntries: function(!ISOBox, !string, !number, !function(!ISOEntry)),
+ * major_brand: string,
+ * minor_version: number,
+ * compatible_brands: Array.,
+ * _data: Array.,
+ * creation_time: number,
+ * modification_time: number,
+ * timescale: number,
+ * duration: number,
+ * rate: number,
+ * volume: number,
+ * reserved1: number,
+ * reserved2: (Array.|number),
+ * matrix: Array.,
+ * pre_defined: (Array.|number),
+ * next_track_ID: number,
+ * track_ID: number,
+ * layer: number,
+ * alternate_group: number,
+ * reserved3: number,
+ * width: number,
+ * height: number,
+ * language: string,
+ * handler_type: string,
+ * name: string,
+ * reserved: Array.,
+ * graphicsmode: number,
+ * opcolor: Array.,
+ * balance: number,
+ * entries: Array.,
+ * location: string,
+ * data_reference_index: number,
+ * pre_defined1: number,
+ * pre_defined2: Array.,
+ * pre_defined3: number,
+ * horizresolution: number,
+ * vertresolution: number,
+ * frame_count: number,
+ * compressorname: Array.,
+ * depth: number,
+ * config: Uint8Array,
+ * channelcount: number,
+ * samplesize: number,
+ * reserved_3: number,
+ * samplerate: number,
+ * esds: Uint8Array,
+ * data_format: number,
+ * scheme_type: number,
+ * scheme_version: number,
+ * default_IsEncrypted: number,
+ * default_IV_size: number,
+ * default_KID: Array.,
+ * default_sample_description_index: number,
+ * default_sample_duration: number,
+ * default_sample_size: number,
+ * default_sample_flags: number,
+ * baseMediaDecodeTime: number,
+ * usertype: ?string,
+ * offset: Array.,
+ * sample_info_size: Array.,
+ * data_offset: number
+ * }}
+ * @property {string} type
+ * @property {number} size
+ * @property {ISOBox} _parent
+ * @property {!Array.} boxes
+ * @property {!Array.} entry
+ * @property {!number} version
+ * @property {!number} flags
+ * @property {!number} sample_count
+ * @property {!number} default_sample_info_size
+ * @property {!number} entry_count
+ * @property {function()} _procFullBox
+ * @property {function(!string, !string, !number)} _procField
+ * @property {function(!string, !number, !string, !number)} _procFieldArray
+ * @property {function(!string, !number, !function(!ISOEntry))} _procEntries
+ * @property {function(!ISOBox, !string, !string, !number)} _procEntryField
+ * @property {function(!ISOBox, !string, !number, !function(!ISOEntry))}
+ * _procSubEntries
+ * @property {string} major_brand
+ * @property {number} minor_version
+ * @property {Array.} compatible_brands
+ * @property {Array.} _data
+ * @property {number} creation_time
+ * @property {number} modification_time
+ * @property {number} timescale
+ * @property {number} duration
+ * @property {number} rate
+ * @property {number} volume
+ * @property {number} reserved1
+ * @property {Array.|number} reserved2
+ * @property {Array.} matrix
+ * @property {Array.|number} pre_defined
+ * @property {number} next_track_ID
+ * @property {number} track_ID
+ * @property {number} layer
+ * @property {number} alternate_group
+ * @property {number} reserved3
+ * @property {number} width
+ * @property {number} height
+ * @property {string} language
+ * @property {string} handler_type
+ * @property {string} name
+ * @property {Array.} reserved
+ * @property {number} graphicsmode
+ * @property {Array.} opcolor
+ * @property {number} balance
+ * @property {Array.} pre_defined2
+ * @property {number} pre_defined3
+ * @property {number} horizresolution
+ * @property {number} vertresolution
+ * @property {number} frame_count
+ * @property {number} compressorname
+ * @property {number} depth
+ * @property {Uint8Array} config
+ * @property {number} channelcount
+ * @property {number} samplesize
+ * @property {number} reserved_3
+ * @property {number} samplerate
+ * @property {Uint8Array} esds
+ * @property {number} data_format
+ * @property {number} scheme_type
+ * @property {number} scheme_version
+ * @property {number} default_IsEncrypted
+ * @property {number} default_IV_size
+ * @property {Array.} default_KID
+ * @property {number} default_sample_description_index
+ * @property {number} default_sample_duration
+ * @property {number} default_sample_size
+ * @property {number} default_sample_flags
+ * @property {number} baseMediaDecodeTime
+ * @property {?string} usertype
+ * @property {Array.|!Array.|
- * undefined)
+ * undefined),
+ * mssPrivateData: (shaka.extern.MssPrivateData|undefined)
* }}
*
* @description
@@ -447,7 +448,34 @@ shaka.extern.FetchCryptoKeysFunction;
* @property {(!Array.|!Array.|
* undefined)} matchedStreams
* The streams in all periods which match the stream. Used for Dash.
+ * @property {(shaka.extern.MssPrivateData|undefined)} mssPrivateData
+ * Microsoft Smooth Streaming only.
+ * Private MSS data that is necessary to be able to do transmuxing.
*
* @exportDoc
*/
shaka.extern.Stream;
+
+
+/**
+ * @typedef {{
+ * duration: number,
+ * timescale: number,
+ * codecPrivateData: ?string
+ * }}
+ *
+ * @description
+ * Private MSS data that is necessary to be able to do transmuxing.
+ *
+ * @property {number} duration
+ * Required.
+ * MSS Stream duration.
+ * @property {number} timescale
+ * Required.
+ * MSS timescale.
+ * @property {?string} codecPrivateData
+ * MSS codecPrivateData.
+ *
+ * @exportDoc
+ */
+shaka.extern.MssPrivateData;
diff --git a/externs/shaka/player.js b/externs/shaka/player.js
index b94c671cdd..1cc0fe9156 100644
--- a/externs/shaka/player.js
+++ b/externs/shaka/player.js
@@ -900,6 +900,29 @@ shaka.extern.DashManifestConfiguration;
shaka.extern.HlsManifestConfiguration;
+/**
+ * @typedef {{
+ * manifestPreprocessor: function(!Element),
+ * sequenceMode: boolean,
+ * keySystemsBySystemId: !Object.
+ * }}
+ *
+ * @property {function(!Element)} manifestPreprocessor
+ * Called immediately after the MSS manifest has been parsed into an
+ * XMLDocument. Provides a way for applications to perform efficient
+ * preprocessing of the manifest.
+ * @property {boolean} sequenceMode
+ * If true, the media segments are appended to the SourceBuffer in
+ * "sequence mode" (ignoring their internal timestamps).
+ * Defaults to false
.
+ * @property {Object.} keySystemsBySystemId
+ * A map of system id to key system name. Defaults to default key systems
+ * mapping handled by Shaka.
+ * @exportDoc
+ */
+shaka.extern.MssManifestConfiguration;
+
+
/**
* @typedef {{
* retryParameters: shaka.extern.RetryParameters,
@@ -911,7 +934,8 @@ shaka.extern.HlsManifestConfiguration;
* defaultPresentationDelay: number,
* segmentRelativeVttTiming: boolean,
* dash: shaka.extern.DashManifestConfiguration,
- * hls: shaka.extern.HlsManifestConfiguration
+ * hls: shaka.extern.HlsManifestConfiguration,
+ * mss: shaka.extern.MssManifestConfiguration
* }}
*
* @property {shaka.extern.RetryParameters} retryParameters
@@ -949,6 +973,8 @@ shaka.extern.HlsManifestConfiguration;
* Advanced parameters used by the DASH manifest parser.
* @property {shaka.extern.HlsManifestConfiguration} hls
* Advanced parameters used by the HLS manifest parser.
+ * @property {shaka.extern.MssManifestConfiguration} mss
+ * Advanced parameters used by the MSS manifest parser.
*
* @exportDoc
*/
diff --git a/externs/shaka/transmuxer.js b/externs/shaka/transmuxer.js
index 091581754d..c61c434b8a 100644
--- a/externs/shaka/transmuxer.js
+++ b/externs/shaka/transmuxer.js
@@ -42,9 +42,12 @@ shaka.extern.Transmuxer = class {
/**
* Transmux a input data to MP4.
* @param {BufferSource} data
+ * @param {shaka.extern.Stream} stream
+ * @param {?shaka.media.SegmentReference} reference The segment reference, or
+ * null for init segments
* @return {!Promise.}
*/
- transmux(data) {}
+ transmux(data, stream, reference) {}
};
diff --git a/karma.conf.js b/karma.conf.js
index ffddd3bb4a..de08c470fa 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -164,6 +164,9 @@ module.exports = (config) => {
// muxjs module next
'node_modules/mux.js/dist/mux.min.js',
+ // codem-isoboxer module next
+ 'node_modules/codem-isoboxer/dist/iso_boxer.min.js',
+
// EME encryption scheme polyfill, compiled into Shaka Player, but outside
// of the Closure deps system, so not in shaka-player.uncompiled.js. This
// is specifically the compiled, minified, cross-browser build of it.
diff --git a/lib/dependencies/all.js b/lib/dependencies/all.js
index faab54c306..cf12fa8b85 100644
--- a/lib/dependencies/all.js
+++ b/lib/dependencies/all.js
@@ -41,6 +41,12 @@ shaka.dependencies = class {
return /** @type {?muxjs} */ (shaka.dependencies.dependencies_.get(
shaka.dependencies.Allowed.muxjs)());
}
+
+ /** @return {?ISOBoxer} */
+ static isoBoxer() {
+ return /** @type {?ISOBoxer} */ (shaka.dependencies.dependencies_.get(
+ shaka.dependencies.Allowed.ISOBoxer)());
+ }
};
/**
@@ -49,6 +55,7 @@ shaka.dependencies = class {
*/
shaka.dependencies.Allowed = {
muxjs: 'muxjs',
+ ISOBoxer: 'ISOBoxer',
};
/**
@@ -59,4 +66,5 @@ shaka.dependencies.Allowed = {
*/
shaka.dependencies.dependencies_ = new Map([
[shaka.dependencies.Allowed.muxjs, () => window.muxjs],
+ [shaka.dependencies.Allowed.ISOBoxer, () => window.ISOBoxer],
]);
diff --git a/lib/media/manifest_parser.js b/lib/media/manifest_parser.js
index 94d2f2b894..7f48da48ad 100644
--- a/lib/media/manifest_parser.js
+++ b/lib/media/manifest_parser.js
@@ -262,6 +262,12 @@ shaka.media.ManifestParser.HLS = 'HLS';
shaka.media.ManifestParser.DASH = 'DASH';
+/**
+ * @const {string}
+ */
+shaka.media.ManifestParser.MSS = 'MSS';
+
+
/**
* @const {string}
*/
diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js
index 8fc60cffec..aa03fc5066 100644
--- a/lib/media/media_source_engine.js
+++ b/lib/media/media_source_engine.js
@@ -602,6 +602,7 @@ shaka.media.MediaSourceEngine = class {
* @param {!BufferSource} data
* @param {?shaka.media.SegmentReference} reference The segment reference
* we are appending, or null for init segments
+ * @param {shaka.extern.Stream} stream
* @param {?boolean} hasClosedCaptions True if the buffer contains CEA closed
* captions
* @param {boolean=} seeked True if we just seeked
@@ -610,7 +611,7 @@ shaka.media.MediaSourceEngine = class {
* @return {!Promise}
*/
async appendBuffer(
- contentType, data, reference, hasClosedCaptions, seeked = false,
+ contentType, data, reference, stream, hasClosedCaptions, seeked = false,
adaptation = false) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
@@ -760,7 +761,8 @@ shaka.media.MediaSourceEngine = class {
}
if (this.transmuxers_[contentType]) {
- data = await this.transmuxers_[contentType].transmux(data);
+ data = await this.transmuxers_[contentType].transmux(
+ data, stream, reference);
}
data = this.workAroundBrokenPlatforms_(
diff --git a/lib/media/segment_reference.js b/lib/media/segment_reference.js
index 796d5c62da..6ab23981aa 100644
--- a/lib/media/segment_reference.js
+++ b/lib/media/segment_reference.js
@@ -10,6 +10,7 @@ goog.provide('shaka.media.SegmentReference');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.util.ArrayUtils');
+goog.require('shaka.util.BufferUtils');
/**
@@ -30,8 +31,10 @@ shaka.media.InitSegmentReference = class {
* @param {null|shaka.extern.MediaQualityInfo=} mediaQuality Information about
* the quality of the media associated with this init segment.
* @param {number=} timescale
+ * @param {(null|BufferSource)=} segmentData
*/
- constructor(uris, startByte, endByte, mediaQuality = null, timescale) {
+ constructor(uris, startByte, endByte, mediaQuality = null, timescale,
+ segmentData = null) {
/** @type {function():!Array.} */
this.getUris = uris;
@@ -46,6 +49,9 @@ shaka.media.InitSegmentReference = class {
/** @type {number|undefined} */
this.timescale = timescale;
+
+ /** @type {BufferSource|null} */
+ this.segmentData = segmentData;
}
/**
@@ -93,6 +99,16 @@ shaka.media.InitSegmentReference = class {
return this.mediaQuality;
}
+ /**
+ * Return the segment data.
+ *
+ * @return {?BufferSource}
+ */
+ getSegmentData() {
+ return this.segmentData;
+ }
+
+
/**
* Check if two initSegmentReference have all the same values.
* @param {?shaka.media.InitSegmentReference} reference1
@@ -101,12 +117,15 @@ shaka.media.InitSegmentReference = class {
*/
static equal(reference1, reference2) {
const ArrayUtils = shaka.util.ArrayUtils;
+ const BufferUtils = shaka.util.BufferUtils;
if (!reference1 || !reference2) {
return reference1 == reference2;
} else {
return reference1.getStartByte() == reference2.getStartByte() &&
reference1.getEndByte() == reference2.getEndByte() &&
- ArrayUtils.equal(reference1.getUris(), reference2.getUris());
+ ArrayUtils.equal(reference1.getUris(), reference2.getUris()) &&
+ BufferUtils.equal(reference1.getSegmentData(),
+ reference2.getSegmentData());
}
}
};
diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js
index 64fccc4365..c7b4b9dbda 100644
--- a/lib/media/streaming_engine.js
+++ b/lib/media/streaming_engine.js
@@ -1713,7 +1713,7 @@ shaka.media.StreamingEngine = class {
mediaState.type, initSegment);
await this.playerInterface_.mediaSourceEngine.appendBuffer(
mediaState.type, initSegment, /* reference= */ null,
- hasClosedCaptions);
+ mediaState.stream, hasClosedCaptions);
} catch (error) {
mediaState.lastInitSegmentReference = null;
throw error;
@@ -1819,6 +1819,7 @@ shaka.media.StreamingEngine = class {
mediaState.type,
segment,
reference,
+ stream,
hasClosedCaptions,
seeked,
adaptation);
@@ -2090,6 +2091,12 @@ shaka.media.StreamingEngine = class {
* @suppress {strictMissingProperties}
*/
async fetch_(mediaState, reference, streamDataCallback, isInit) {
+ if (reference instanceof shaka.media.InitSegmentReference) {
+ const segmentData = reference.getSegmentData();
+ if (segmentData) {
+ return segmentData;
+ }
+ }
let op = null;
if (
mediaState.segmentPrefetch &&
diff --git a/lib/mss/content_protection.js b/lib/mss/content_protection.js
new file mode 100644
index 0000000000..d63a8f6859
--- /dev/null
+++ b/lib/mss/content_protection.js
@@ -0,0 +1,343 @@
+/*! @license
+ * Shaka Player
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+goog.provide('shaka.mss.ContentProtection');
+
+goog.require('shaka.log');
+goog.require('shaka.util.BufferUtils');
+goog.require('shaka.util.ManifestParserUtils');
+goog.require('shaka.util.Pssh');
+goog.require('shaka.util.StringUtils');
+goog.require('shaka.util.Uint8ArrayUtils');
+goog.require('shaka.util.XmlUtils');
+
+
+/**
+ * @summary A set of functions for parsing and interpreting Protection
+ * elements.
+ */
+shaka.mss.ContentProtection = class {
+ /**
+ * Parses info from the Protection elements.
+ *
+ * @param {!Array.} elems
+ * @param {!Object.} keySystemsBySystemId
+ * @return {!Array.}
+ */
+ static parseFromProtection(elems, keySystemsBySystemId) {
+ const ContentProtection = shaka.mss.ContentProtection;
+ const XmlUtils = shaka.util.XmlUtils;
+
+ /** @type {!Array.} */
+ let protectionHeader = [];
+ for (const elem of elems) {
+ protectionHeader = protectionHeader.concat(
+ XmlUtils.findChildren(elem, 'ProtectionHeader'));
+ }
+ if (!protectionHeader.length) {
+ return [];
+ }
+ return ContentProtection.convertElements_(
+ protectionHeader, keySystemsBySystemId);
+ }
+
+ /**
+ * Parses an Array buffer starting at byteOffset for PlayReady Object Records.
+ * Each PRO Record is preceded by its PlayReady Record type and length in
+ * bytes.
+ *
+ * PlayReady Object Record format: https://goo.gl/FTcu46
+ *
+ * @param {!DataView} view
+ * @param {number} byteOffset
+ * @return {!Array.}
+ * @private
+ */
+ static parseMsProRecords_(view, byteOffset) {
+ const records = [];
+
+ while (byteOffset < view.byteLength - 1) {
+ const type = view.getUint16(byteOffset, true);
+ byteOffset += 2;
+
+ const byteLength = view.getUint16(byteOffset, true);
+ byteOffset += 2;
+
+ if ((byteLength & 1) != 0 || byteLength + byteOffset > view.byteLength) {
+ shaka.log.warning('Malformed MS PRO object');
+ return [];
+ }
+
+ const recordValue = shaka.util.BufferUtils.toUint8(
+ view, byteOffset, byteLength);
+ records.push({
+ type: type,
+ value: recordValue,
+ });
+
+ byteOffset += byteLength;
+ }
+
+ return records;
+ }
+
+ /**
+ * Parses a buffer for PlayReady Objects. The data
+ * should contain a 32-bit integer indicating the length of
+ * the PRO in bytes. Following that, a 16-bit integer for
+ * the number of PlayReady Object Records in the PRO. Lastly,
+ * a byte array of the PRO Records themselves.
+ *
+ * PlayReady Object format: https://goo.gl/W8yAN4
+ *
+ * @param {BufferSource} data
+ * @return {!Array.}
+ * @private
+ */
+ static parseMsPro_(data) {
+ let byteOffset = 0;
+ const view = shaka.util.BufferUtils.toDataView(data);
+
+ // First 4 bytes is the PRO length (DWORD)
+ const byteLength = view.getUint32(byteOffset, /* littleEndian= */ true);
+ byteOffset += 4;
+
+ if (byteLength != data.byteLength) {
+ // Malformed PRO
+ shaka.log.warning('PlayReady Object with invalid length encountered.');
+ return [];
+ }
+
+ // Skip PRO Record count (WORD)
+ byteOffset += 2;
+
+ // Rest of the data contains the PRO Records
+ const ContentProtection = shaka.mss.ContentProtection;
+ return ContentProtection.parseMsProRecords_(view, byteOffset);
+ }
+
+ /**
+ * Parse a PlayReady Header format: https://goo.gl/dBzxNA
+ * a try to find the LA_URL value.
+ *
+ * @param {!Element} xml
+ * @return {string}
+ * @private
+ */
+ static getLaurl_(xml) {
+ // LA_URL element is optional and no more than one is
+ // allowed inside the DATA element. Only absolute URLs are allowed.
+ // If the LA_URL element exists, it must not be empty.
+ for (const elem of xml.getElementsByTagName('DATA')) {
+ const laUrl = shaka.util.XmlUtils.findChild(elem, 'LA_URL');
+ if (laUrl) {
+ return laUrl.textContent;
+ }
+ }
+
+ // Not found
+ // We return a empty string instead null because is the default value for
+ // a License in our model.
+ return '';
+ }
+
+ /**
+ * Gets a PlayReady license URL from a protection element
+ * containing a PlayReady Header Object
+ *
+ * @param {!Element} element
+ * @return {string}
+ */
+ static getPlayReadyLicenseUrl(element) {
+ const ContentProtection = shaka.mss.ContentProtection;
+ const rootElement = ContentProtection.getPlayReadyHeaderObject_(element);
+ if (!rootElement) {
+ return '';
+ }
+
+ return ContentProtection.getLaurl_(rootElement);
+ }
+
+ /**
+ * Parse a PlayReady Header format: https://goo.gl/dBzxNA
+ * a try to find the KID value.
+ *
+ * @param {!Element} xml
+ * @return {?string}
+ * @private
+ */
+ static getKID_(xml) {
+ // KID element is optional and no more than one is
+ // allowed inside the DATA element.
+ for (const elem of xml.getElementsByTagName('DATA')) {
+ const kid = shaka.util.XmlUtils.findChild(elem, 'KID');
+ if (kid) {
+ return kid.textContent;
+ }
+ }
+
+ // Not found
+ return null;
+ }
+
+ /**
+ * Gets a PlayReady KID from a protection element
+ * containing a PlayReady Header Object
+ *
+ * @param {!Element} element
+ * @return {?string}
+ * @private
+ */
+ static getPlayReadyKID_(element) {
+ const ContentProtection = shaka.mss.ContentProtection;
+ const rootElement = ContentProtection.getPlayReadyHeaderObject_(element);
+ if (!rootElement) {
+ return null;
+ }
+
+ return ContentProtection.getKID_(rootElement);
+ }
+
+ /**
+ * Gets a PlayReady Header Object from a protection element
+ *
+ * @param {!Element} element
+ * @return {?Element}
+ * @private
+ */
+ static getPlayReadyHeaderObject_(element) {
+ const ContentProtection = shaka.mss.ContentProtection;
+ const PLAYREADY_RECORD_TYPES = ContentProtection.PLAYREADY_RECORD_TYPES;
+
+ const bytes = shaka.util.Uint8ArrayUtils.fromBase64(element.textContent);
+ const records = ContentProtection.parseMsPro_(bytes);
+ const record = records.filter((record) => {
+ return record.type === PLAYREADY_RECORD_TYPES.RIGHTS_MANAGEMENT;
+ })[0];
+
+ if (!record) {
+ return null;
+ }
+
+ const xml = shaka.util.StringUtils.fromUTF16(record.value, true);
+ const rootElement = shaka.util.XmlUtils.parseXmlString(xml, 'WRMHEADER');
+ if (!rootElement) {
+ return null;
+ }
+ return rootElement;
+ }
+
+ /**
+ * Gets a initData from a protection element.
+ *
+ * @param {!Element} element
+ * @param {string} systemID
+ * @param {?string} keyId
+ * @return {?Array.}
+ * @private
+ */
+ static getInitDataFromPro_(element, systemID, keyId) {
+ const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils;
+ const data = Uint8ArrayUtils.fromBase64(element.textContent);
+ const systemId = Uint8ArrayUtils.fromHex(systemID.replace(/-/g, ''));
+ const keyIds = new Set();
+ const psshVersion = 0;
+ const pssh =
+ shaka.util.Pssh.createPssh(data, systemId, keyIds, psshVersion);
+ return [
+ {
+ initData: pssh,
+ initDataType: 'cenc',
+ keyId: keyId,
+ },
+ ];
+ }
+
+ /**
+ * Creates DrmInfo objects from an array of elements.
+ *
+ * @param {!Array.} elements
+ * @param {!Object.} keySystemsBySystemId
+ * @return {!Array.}
+ * @private
+ */
+ static convertElements_(elements, keySystemsBySystemId) {
+ const ContentProtection = shaka.mss.ContentProtection;
+ const ManifestParserUtils = shaka.util.ManifestParserUtils;
+ const licenseUrlParsers = ContentProtection.licenseUrlParsers_;
+
+ /** @type {!Array.} */
+ const out = [];
+
+ for (let i = 0; i < elements.length; i++) {
+ const element = elements[i];
+ const systemID = element.getAttribute('SystemID');
+ const keySystem = keySystemsBySystemId[systemID];
+ if (keySystem) {
+ const KID = ContentProtection.getPlayReadyKID_(element);
+ const initData = ContentProtection.getInitDataFromPro_(
+ element, systemID, KID);
+
+ const info = ManifestParserUtils.createDrmInfo(keySystem, initData);
+ if (KID) {
+ info.keyIds.add(KID);
+ }
+
+ const licenseParser = licenseUrlParsers.get(keySystem);
+ if (licenseParser) {
+ info.licenseServerUri = licenseParser(element);
+ }
+
+ out.push(info);
+ }
+ }
+
+ return out;
+ }
+};
+
+/**
+ * @typedef {{
+ * type: number,
+ * value: !Uint8Array
+ * }}
+ *
+ * @description
+ * The parsed result of a PlayReady object record.
+ *
+ * @property {number} type
+ * Type of data stored in the record.
+ * @property {!Uint8Array} value
+ * Record content.
+ */
+shaka.mss.ContentProtection.PlayReadyRecord;
+
+/**
+ * Enum for PlayReady record types.
+ * @enum {number}
+ */
+shaka.mss.ContentProtection.PLAYREADY_RECORD_TYPES = {
+ RIGHTS_MANAGEMENT: 0x001,
+ RESERVED: 0x002,
+ EMBEDDED_LICENSE: 0x003,
+};
+
+/**
+ * A map of key system name to license server url parser.
+ *
+ * @const {!Map.}
+ * @private
+ */
+shaka.mss.ContentProtection.licenseUrlParsers_ = new Map()
+ .set('com.microsoft.playready',
+ shaka.mss.ContentProtection.getPlayReadyLicenseUrl)
+ .set('com.microsoft.playready.recommendation',
+ shaka.mss.ContentProtection.getPlayReadyLicenseUrl)
+ .set('com.microsoft.playready.software',
+ shaka.mss.ContentProtection.getPlayReadyLicenseUrl)
+ .set('com.microsoft.playready.hardware',
+ shaka.mss.ContentProtection.getPlayReadyLicenseUrl);
+
diff --git a/lib/mss/mss_parser.js b/lib/mss/mss_parser.js
new file mode 100644
index 0000000000..b86ef6d933
--- /dev/null
+++ b/lib/mss/mss_parser.js
@@ -0,0 +1,1051 @@
+/*! @license
+ * Shaka Player
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+goog.provide('shaka.mss.MssParser');
+
+goog.require('goog.asserts');
+goog.require('shaka.abr.Ewma');
+goog.require('shaka.log');
+goog.require('shaka.media.InitSegmentReference');
+goog.require('shaka.media.ManifestParser');
+goog.require('shaka.media.PresentationTimeline');
+goog.require('shaka.media.SegmentIndex');
+goog.require('shaka.media.SegmentReference');
+goog.require('shaka.mss.ContentProtection');
+goog.require('shaka.mss.MssUtils');
+goog.require('shaka.net.NetworkingEngine');
+goog.require('shaka.util.Error');
+goog.require('shaka.util.ManifestParserUtils');
+goog.require('shaka.util.OperationManager');
+goog.require('shaka.util.Timer');
+goog.require('shaka.util.XmlUtils');
+goog.require('shaka.dependencies');
+
+
+/**
+ * Creates a new MSS parser.
+ *
+ * @implements {shaka.extern.ManifestParser}
+ * @export
+ */
+shaka.mss.MssParser = class {
+ /** Creates a new MSS parser. */
+ constructor() {
+ /** @private {?shaka.extern.ManifestConfiguration} */
+ this.config_ = null;
+
+ /** @private {?shaka.extern.ManifestParser.PlayerInterface} */
+ this.playerInterface_ = null;
+
+ /** @private {!Array.} */
+ this.manifestUris_ = [];
+
+ /** @private {?shaka.extern.Manifest} */
+ this.manifest_ = null;
+
+ /** @private {number} */
+ this.globalId_ = 1;
+
+ /**
+ * The update period in seconds, or 0 for no updates.
+ * @private {number}
+ */
+ this.updatePeriod_ = 0;
+
+ /**
+ * An ewma that tracks how long updates take.
+ * This is to mitigate issues caused by slow parsing on embedded devices.
+ * @private {!shaka.abr.Ewma}
+ */
+ this.averageUpdateDuration_ = new shaka.abr.Ewma(5);
+
+ /** @private {shaka.util.Timer} */
+ this.updateTimer_ = new shaka.util.Timer(() => {
+ this.onUpdate_();
+ });
+
+ /** @private {!shaka.util.OperationManager} */
+ this.operationManager_ = new shaka.util.OperationManager();
+
+ /**
+ * @private {!Map.}
+ */
+ this.initSegmentDataByStreamId_ = new Map();
+ }
+
+ /**
+ * @override
+ * @exportInterface
+ */
+ configure(config) {
+ goog.asserts.assert(config.mss != null,
+ 'MssManifestConfiguration should not be null!');
+
+ this.config_ = config;
+ }
+
+ /**
+ * @override
+ * @exportInterface
+ */
+ async start(uri, playerInterface) {
+ goog.asserts.assert(this.config_, 'Must call configure() before start()!');
+ this.manifestUris_ = [uri];
+ this.playerInterface_ = playerInterface;
+
+ await this.requestManifest_();
+
+ // Make sure that the parser has not been destroyed.
+ if (!this.playerInterface_) {
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.CRITICAL,
+ shaka.util.Error.Category.PLAYER,
+ shaka.util.Error.Code.OPERATION_ABORTED);
+ }
+
+ this.setUpdateTimer_();
+
+ goog.asserts.assert(this.manifest_, 'Manifest should be non-null!');
+ return this.manifest_;
+ }
+
+ /**
+ * Called when the update timer ticks.
+ *
+ * @return {!Promise}
+ * @private
+ */
+ async onUpdate_() {
+ goog.asserts.assert(this.updatePeriod_ >= 0,
+ 'There should be an update period');
+
+ shaka.log.info('Updating manifest...');
+
+ try {
+ await this.requestManifest_();
+ } catch (error) {
+ goog.asserts.assert(error instanceof shaka.util.Error,
+ 'Should only receive a Shaka error');
+
+ // Try updating again, but ensure we haven't been destroyed.
+ if (this.playerInterface_) {
+ // We will retry updating, so override the severity of the error.
+ error.severity = shaka.util.Error.Severity.RECOVERABLE;
+ this.playerInterface_.onError(error);
+ }
+ }
+
+ // Detect a call to stop()
+ if (!this.playerInterface_) {
+ return;
+ }
+
+ this.setUpdateTimer_();
+ }
+
+ /**
+ * Sets the update timer. Does nothing if the manifest is not live.
+ *
+ * @private
+ */
+ setUpdateTimer_() {
+ if (this.updatePeriod_ <= 0) {
+ return;
+ }
+
+ const finalDelay = Math.max(
+ shaka.mss.MssParser.MIN_UPDATE_PERIOD_,
+ this.updatePeriod_,
+ this.averageUpdateDuration_.getEstimate());
+
+ // We do not run the timer as repeating because part of update is async and
+ // we need schedule the update after it finished.
+ this.updateTimer_.tickAfter(/* seconds= */ finalDelay);
+ }
+
+ /**
+ * @override
+ * @exportInterface
+ */
+ stop() {
+ this.playerInterface_ = null;
+ this.config_ = null;
+ this.manifestUris_ = [];
+ this.manifest_ = null;
+
+ if (this.updateTimer_ != null) {
+ this.updateTimer_.stop();
+ this.updateTimer_ = null;
+ }
+
+ this.initSegmentDataByStreamId_.clear();
+
+ return this.operationManager_.destroy();
+ }
+
+ /**
+ * @override
+ * @exportInterface
+ */
+ async update() {
+ try {
+ await this.requestManifest_();
+ } catch (error) {
+ if (!this.playerInterface_ || !error) {
+ return;
+ }
+ goog.asserts.assert(error instanceof shaka.util.Error, 'Bad error type');
+ this.playerInterface_.onError(error);
+ }
+ }
+
+ /**
+ * @override
+ * @exportInterface
+ */
+ onExpirationUpdated(sessionId, expiration) {
+ // No-op
+ }
+
+ /**
+ * Makes a network request for the manifest and parses the resulting data.
+ *
+ * @private
+ */
+ async requestManifest_() {
+ const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
+ const type = shaka.net.NetworkingEngine.AdvancedRequestType.MSS;
+ const request = shaka.net.NetworkingEngine.makeRequest(
+ this.manifestUris_, this.config_.retryParameters);
+ const networkingEngine = this.playerInterface_.networkingEngine;
+
+ const startTime = Date.now();
+ const operation = networkingEngine.request(requestType, request, {type});
+ this.operationManager_.manage(operation);
+
+ const response = await operation.promise;
+
+ // Detect calls to stop().
+ if (!this.playerInterface_) {
+ return;
+ }
+
+ // For redirections add the response uri to the first entry in the
+ // Manifest Uris array.
+ if (response.uri && !this.manifestUris_.includes(response.uri)) {
+ this.manifestUris_.unshift(response.uri);
+ }
+
+ // This may throw, but it will result in a failed promise.
+ this.parseManifest_(response.data, response.uri);
+ // Keep track of how long the longest manifest update took.
+ const endTime = Date.now();
+ const updateDuration = (endTime - startTime) / 1000.0;
+ this.averageUpdateDuration_.sample(1, updateDuration);
+ }
+
+ /**
+ * Parses the manifest XML. This also handles updates and will update the
+ * stored manifest.
+ *
+ * @param {BufferSource} data
+ * @param {string} finalManifestUri The final manifest URI, which may
+ * differ from this.manifestUri_ if there has been a redirect.
+ * @return {!Promise}
+ * @private
+ */
+ parseManifest_(data, finalManifestUri) {
+ const mss = shaka.util.XmlUtils.parseXml(data, 'SmoothStreamingMedia');
+ if (!mss) {
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.CRITICAL,
+ shaka.util.Error.Category.MANIFEST,
+ shaka.util.Error.Code.MSS_INVALID_XML,
+ finalManifestUri);
+ }
+ this.processManifest_(mss, finalManifestUri);
+ return Promise.resolve();
+ }
+
+
+ /**
+ * Takes a formatted MSS and converts it into a manifest.
+ *
+ * @param {!Element} mss
+ * @param {string} finalManifestUri The final manifest URI, which may
+ * differ from this.manifestUri_ if there has been a redirect.
+ * @private
+ */
+ processManifest_(mss, finalManifestUri) {
+ const XmlUtils = shaka.util.XmlUtils;
+
+ const manifestPreprocessor = this.config_.mss.manifestPreprocessor;
+ if (manifestPreprocessor) {
+ manifestPreprocessor(mss);
+ }
+
+ /** @type {!shaka.media.PresentationTimeline} */
+ let presentationTimeline;
+ if (this.manifest_) {
+ presentationTimeline = this.manifest_.presentationTimeline;
+ } else {
+ presentationTimeline = new shaka.media.PresentationTimeline(
+ /* presentationStartTime= */ null, /* delay= */ 0);
+ }
+
+ const isLive = XmlUtils.parseAttr(mss, 'IsLive',
+ XmlUtils.parseBoolean, /* defaultValue= */ false);
+
+ if (isLive) {
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.CRITICAL,
+ shaka.util.Error.Category.MANIFEST,
+ shaka.util.Error.Code.MSS_LIVE_CONTENT_NOT_SUPPORTED);
+ }
+
+ presentationTimeline.setStatic(!isLive);
+
+ const timescale = XmlUtils.parseAttr(mss, 'TimeScale',
+ XmlUtils.parseNonNegativeInt, shaka.mss.MssParser.DEFAULT_TIME_SCALE_);
+ goog.asserts.assert(timescale && timescale >= 0,
+ 'Timescale must be defined!');
+
+ let dvrWindowLength = XmlUtils.parseAttr(mss, 'DVRWindowLength',
+ XmlUtils.parseNonNegativeInt);
+ // If the DVRWindowLength field is omitted for a live presentation or set
+ // to 0, the DVR window is effectively infinite
+ if (isLive && (dvrWindowLength === 0 || isNaN(dvrWindowLength))) {
+ dvrWindowLength = Infinity;
+ }
+ // Start-over
+ const canSeek = XmlUtils.parseAttr(mss, 'CanSeek',
+ XmlUtils.parseBoolean, /* defaultValue= */ false);
+ if (dvrWindowLength === 0 && canSeek) {
+ dvrWindowLength = Infinity;
+ }
+
+ let segmentAvailabilityDuration = null;
+ if (dvrWindowLength && dvrWindowLength > 0) {
+ segmentAvailabilityDuration = dvrWindowLength / timescale;
+ }
+
+ // If it's live, we check for an override.
+ if (isLive && !isNaN(this.config_.availabilityWindowOverride)) {
+ segmentAvailabilityDuration = this.config_.availabilityWindowOverride;
+ }
+
+ // If it's null, that means segments are always available. This is always
+ // the case for VOD, and sometimes the case for live.
+ if (segmentAvailabilityDuration == null) {
+ segmentAvailabilityDuration = Infinity;
+ }
+
+ presentationTimeline.setSegmentAvailabilityDuration(
+ segmentAvailabilityDuration);
+
+ // Duration in timescale units.
+ const duration = XmlUtils.parseAttr(mss, 'Duration',
+ XmlUtils.parseNonNegativeInt, Infinity);
+ goog.asserts.assert(duration && duration >= 0,
+ 'Duration must be defined!');
+
+ if (!isLive) {
+ presentationTimeline.setDuration(duration / timescale);
+ }
+
+ /** @type {!shaka.mss.MssParser.Context} */
+ const context = {
+ variants: [],
+ textStreams: [],
+ timescale: timescale,
+ duration: duration / timescale,
+ };
+
+ this.parseStreamIndexes_(mss, context);
+
+ // These steps are not done on manifest update.
+ if (!this.manifest_) {
+ this.manifest_ = {
+ presentationTimeline: presentationTimeline,
+ variants: context.variants,
+ textStreams: context.textStreams,
+ imageStreams: [],
+ offlineSessionIds: [],
+ minBufferTime: 0,
+ sequenceMode: this.config_.mss.sequenceMode,
+ ignoreManifestTimestampsInSegmentsMode: false,
+ type: shaka.media.ManifestParser.MSS,
+ };
+
+ // This is the first point where we have a meaningful presentation start
+ // time, and we need to tell PresentationTimeline that so that it can
+ // maintain consistency from here on.
+ presentationTimeline.lockStartTime();
+ } else {
+ // Just update the variants and text streams.
+ this.manifest_.variants = context.variants;
+ this.manifest_.textStreams = context.textStreams;
+
+ // Re-filter the manifest. This will check any configured restrictions on
+ // new variants, and will pass any new init data to DrmEngine to ensure
+ // that key rotation works correctly.
+ this.playerInterface_.filter(this.manifest_);
+ }
+ }
+
+ /**
+ * @param {!Element} mss
+ * @param {!shaka.mss.MssParser.Context} context
+ * @private
+ */
+ parseStreamIndexes_(mss, context) {
+ const ContentProtection = shaka.mss.ContentProtection;
+ const XmlUtils = shaka.util.XmlUtils;
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
+
+ const protectionElems = XmlUtils.findChildren(mss, 'Protection');
+ const drmInfos = ContentProtection.parseFromProtection(
+ protectionElems, this.config_.mss.keySystemsBySystemId);
+
+ const audioStreams = [];
+ const videoStreams = [];
+ const textStreams = [];
+ const streamIndexes = XmlUtils.findChildren(mss, 'StreamIndex');
+ for (const streamIndex of streamIndexes) {
+ const qualityLevels = XmlUtils.findChildren(streamIndex, 'QualityLevel');
+ const timeline = this.createTimeline_(
+ streamIndex, context.timescale, context.duration);
+ // For each QualityLevel node, create a stream element
+ for (const qualityLevel of qualityLevels) {
+ const stream = this.createStream_(
+ streamIndex, qualityLevel, timeline, drmInfos, context);
+ if (!stream) {
+ // Skip unsupported stream
+ continue;
+ }
+ if (stream.type == ContentType.AUDIO &&
+ !this.config_.disableAudio) {
+ audioStreams.push(stream);
+ } else if (stream.type == ContentType.VIDEO &&
+ !this.config_.disableVideo) {
+ videoStreams.push(stream);
+ } else if (stream.type == ContentType.TEXT &&
+ !this.config_.disableText) {
+ textStreams.push(stream);
+ }
+ }
+ }
+
+ const variants = [];
+ for (const audio of (audioStreams.length > 0 ? audioStreams : [null])) {
+ for (const video of (videoStreams.length > 0 ? videoStreams : [null])) {
+ variants.push(this.createVariant_(audio, video));
+ }
+ }
+ context.variants = variants;
+ context.textStreams = textStreams;
+ }
+
+ /**
+ * @param {!Element} streamIndex
+ * @param {!Element} qualityLevel
+ * @param {!Array.} timeline
+ * @param {!Array.} drmInfos
+ * @param {!shaka.mss.MssParser.Context} context
+ * @return {?shaka.extern.Stream}
+ * @private
+ */
+ createStream_(streamIndex, qualityLevel, timeline, drmInfos, context) {
+ const XmlUtils = shaka.util.XmlUtils;
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
+ const MssParser = shaka.mss.MssParser;
+
+ const type = streamIndex.getAttribute('Type');
+ const isValidType = type === 'audio' || type === 'video' ||
+ type === 'text';
+ if (!isValidType) {
+ shaka.log.alwaysWarn('Ignoring unrecognized type:', type);
+ return null;
+ }
+
+ const lang = streamIndex.getAttribute('Language');
+ const id = this.globalId_++;
+
+ const bandwidth = XmlUtils.parseAttr(
+ qualityLevel, 'Bitrate', XmlUtils.parsePositiveInt);
+ const width = XmlUtils.parseAttr(
+ qualityLevel, 'MaxWidth', XmlUtils.parsePositiveInt);
+ const height = XmlUtils.parseAttr(
+ qualityLevel, 'MaxHeight', XmlUtils.parsePositiveInt);
+ const channelsCount = XmlUtils.parseAttr(
+ qualityLevel, 'Channels', XmlUtils.parsePositiveInt);
+ const audioSamplingRate = XmlUtils.parseAttr(
+ qualityLevel, 'SamplingRate', XmlUtils.parsePositiveInt);
+
+ /** @type {!shaka.extern.Stream} */
+ const stream = {
+ id: id,
+ originalId: streamIndex.getAttribute('Name') || String(id),
+ createSegmentIndex: () => Promise.resolve(),
+ closeSegmentIndex: () => Promise.resolve(),
+ segmentIndex: null,
+ mimeType: '',
+ codecs: '',
+ frameRate: undefined,
+ pixelAspectRatio: undefined,
+ bandwidth: bandwidth || 0,
+ width: width || undefined,
+ height: height || undefined,
+ kind: '',
+ encrypted: drmInfos.length > 0,
+ drmInfos: drmInfos,
+ keyIds: new Set(),
+ language: lang || 'und',
+ label: '',
+ type: '',
+ primary: false,
+ trickModeVideo: null,
+ emsgSchemeIdUris: [],
+ roles: [],
+ forced: false,
+ channelsCount: channelsCount,
+ audioSamplingRate: audioSamplingRate,
+ spatialAudio: false,
+ closedCaptions: null,
+ hdr: undefined,
+ tilesLayout: undefined,
+ matchedStreams: [],
+ mssPrivateData: {
+ duration: context.duration,
+ timescale: context.timescale,
+ codecPrivateData: null,
+ },
+ };
+
+ // This is specifically for text tracks.
+ const subType = streamIndex.getAttribute('Subtype');
+ if (subType) {
+ const role = MssParser.ROLE_MAPPING_[subType];
+ if (role) {
+ stream.roles.push(role);
+ }
+ if (role === 'main') {
+ stream.primary = true;
+ }
+ }
+
+ let fourCCValue = qualityLevel.getAttribute('FourCC');
+
+ // If FourCC not defined at QualityLevel level,
+ // then get it from StreamIndex level
+ if (fourCCValue === null || fourCCValue === '') {
+ fourCCValue = streamIndex.getAttribute('FourCC');
+ }
+
+ // If still not defined (optional for audio stream,
+ // see https://msdn.microsoft.com/en-us/library/ff728116%28v=vs.95%29.aspx),
+ // then we consider the stream is an audio AAC stream
+ if (!fourCCValue) {
+ if (type === 'audio') {
+ fourCCValue = 'AAC';
+ } else if (type === 'video') {
+ shaka.log.alwaysWarn('FourCC is not defined whereas it is required ' +
+ 'for a QualityLevel element for a StreamIndex of type "video"');
+ return null;
+ }
+ }
+
+ // Check if codec is supported
+ if (!MssParser.SUPPORTED_CODECS_.includes(fourCCValue.toUpperCase())) {
+ shaka.log.alwaysWarn('Codec not supported:', fourCCValue);
+ return null;
+ }
+
+ const codecPrivateData = this.getCodecPrivateData_(
+ qualityLevel, type, fourCCValue, stream);
+ stream.mssPrivateData.codecPrivateData = codecPrivateData;
+
+ switch (type) {
+ case 'audio':
+ if (!codecPrivateData) {
+ shaka.log.alwaysWarn('Quality unsupported without CodecPrivateData',
+ type);
+ return null;
+ }
+ stream.type = ContentType.AUDIO;
+ // This mimetype is fake to allow the transmuxing.
+ stream.mimeType = 'mss/audio/mp4';
+ stream.codecs = this.getAACCodec_(
+ qualityLevel, fourCCValue, codecPrivateData);
+ break;
+ case 'video':
+ if (!codecPrivateData) {
+ shaka.log.alwaysWarn('Quality unsupported without CodecPrivateData',
+ type);
+ return null;
+ }
+ stream.type = ContentType.VIDEO;
+ // This mimetype is fake to allow the transmuxing.
+ stream.mimeType = 'mss/video/mp4';
+ stream.codecs = this.getH264Codec_(
+ qualityLevel, codecPrivateData);
+ break;
+ case 'text':
+ stream.type = ContentType.TEXT;
+ stream.mimeType = 'application/mp4';
+ if (fourCCValue === 'TTML' || fourCCValue === 'DFXP') {
+ stream.codecs = 'stpp';
+ }
+ break;
+ }
+
+ // Lazy-Load the segment index to avoid create all init segment at the
+ // same time
+ stream.createSegmentIndex = () => {
+ if (stream.segmentIndex) {
+ return Promise.resolve();
+ }
+ let initSegmentData;
+ if (this.initSegmentDataByStreamId_.has(stream.id)) {
+ initSegmentData = this.initSegmentDataByStreamId_.get(stream.id);
+ } else {
+ initSegmentData = shaka.mss.MssUtils.generateInitSegment(stream);
+ this.initSegmentDataByStreamId_.set(stream.id, initSegmentData);
+ }
+ const initSegmentRef = new shaka.media.InitSegmentReference(
+ () => [],
+ /* startByte= */ 0,
+ /* endByte= */ null,
+ /* mediaQuality= */ null,
+ /* timescale= */ undefined,
+ initSegmentData);
+
+ const segments = this.createSegments_(initSegmentRef,
+ stream, streamIndex, timeline, context);
+
+ stream.segmentIndex = new shaka.media.SegmentIndex(segments);
+ return Promise.resolve();
+ };
+ stream.closeSegmentIndex = () => {
+ // If we have a segment index, release it.
+ if (stream.segmentIndex) {
+ stream.segmentIndex.release();
+ stream.segmentIndex = null;
+ }
+ };
+
+ return stream;
+ }
+
+ /**
+ * @param {!Element} qualityLevel
+ * @param {string} type
+ * @param {string} fourCCValue
+ * @param {!shaka.extern.Stream} stream
+ * @return {?string}
+ * @private
+ */
+ getCodecPrivateData_(qualityLevel, type, fourCCValue, stream) {
+ const codecPrivateData = qualityLevel.getAttribute('CodecPrivateData');
+ if (codecPrivateData) {
+ return codecPrivateData;
+ }
+ if (type !== 'audio') {
+ return null;
+ }
+ // For the audio we can reconstruct the CodecPrivateData
+ // By default stereo
+ const channels = stream.channelsCount || 2;
+ // By default 44,1kHz.
+ const samplingRate = stream.audioSamplingRate || 44100;
+
+ const samplingFrequencyIndex = {
+ 96000: 0x0,
+ 88200: 0x1,
+ 64000: 0x2,
+ 48000: 0x3,
+ 44100: 0x4,
+ 32000: 0x5,
+ 24000: 0x6,
+ 22050: 0x7,
+ 16000: 0x8,
+ 12000: 0x9,
+ 11025: 0xA,
+ 8000: 0xB,
+ 7350: 0xC,
+ };
+
+ const indexFreq = samplingFrequencyIndex[samplingRate];
+ if (fourCCValue === 'AACH') {
+ // High Efficiency AAC Profile
+ const objectType = 0x05;
+ // 4 bytes :
+ // XXXXX XXXX XXXX XXXX
+ // 'ObjectType' 'Freq Index' 'Channels value' 'Extens Sampl Freq'
+ // XXXXX XXX XXXXXXX
+ // 'ObjectType' 'GAS' 'alignment = 0'
+ const data = new Uint8Array(4);
+ // In HE AAC Extension Sampling frequence
+ // equals to SamplingRate * 2
+ const extensionSamplingFrequencyIndex =
+ samplingFrequencyIndex[samplingRate * 2];
+ // Freq Index is present for 3 bits in the first byte, last bit is in
+ // the second
+ data[0] = (objectType << 3) | (indexFreq >> 1);
+ data[1] = (indexFreq << 7) | (channels << 3) |
+ (extensionSamplingFrequencyIndex >> 1);
+ // Origin object type equals to 2 => AAC Main Low Complexity
+ data[2] = (extensionSamplingFrequencyIndex << 7) | (0x02 << 2);
+ // Slignment bits
+ data[3] = 0x0;
+ // Put the 4 bytes in an 16 bits array
+ const arr16 = new Uint16Array(2);
+ arr16[0] = (data[0] << 8) + data[1];
+ arr16[1] = (data[2] << 8) + data[3];
+ // Convert decimal to hex value
+ return arr16[0].toString(16) + arr16[1].toString(16);
+ } else {
+ // AAC Main Low Complexity
+ const objectType = 0x02;
+ // 2 bytes:
+ // XXXXX XXXX XXXX XXX
+ // 'ObjectType' 'Freq Index' 'Channels value' 'GAS = 000'
+ const data = new Uint8Array(2);
+ // Freq Index is present for 3 bits in the first byte, last bit is in
+ // the second
+ data[0] = (objectType << 3) | (indexFreq >> 1);
+ data[1] = (indexFreq << 7) | (channels << 3);
+ // Put the 2 bytes in an 16 bits array
+ const arr16 = new Uint16Array(1);
+ arr16[0] = (data[0] << 8) + data[1];
+ // Convert decimal to hex value
+ return arr16[0].toString(16);
+ }
+ }
+
+ /**
+ * @param {!Element} qualityLevel
+ * @param {string} fourCCValue
+ * @param {?string} codecPrivateData
+ * @return {string}
+ * @private
+ */
+ getAACCodec_(qualityLevel, fourCCValue, codecPrivateData) {
+ let objectType = 0;
+
+ // Chrome problem, in implicit AAC HE definition, so when AACH is detected
+ // in FourCC set objectType to 5 => strange, it should be 2
+ if (fourCCValue === 'AACH') {
+ objectType = 0x05;
+ }
+ if (!codecPrivateData) {
+ // AAC Main Low Complexity => object Type = 2
+ objectType = 0x02;
+ if (fourCCValue === 'AACH') {
+ // High Efficiency AAC Profile = object Type = 5 SBR
+ objectType = 0x05;
+ }
+ } else if (objectType === 0) {
+ objectType = (parseInt(codecPrivateData.substr(0, 2), 16) & 0xF8) >> 3;
+ }
+
+ return 'mp4a.40.' + objectType;
+ }
+
+ /**
+ * @param {!Element} qualityLevel
+ * @param {?string} codecPrivateData
+ * @return {string}
+ * @private
+ */
+ getH264Codec_(qualityLevel, codecPrivateData) {
+ // Extract from the CodecPrivateData field the hexadecimal representation
+ // of the following three bytes in the sequence parameter set NAL unit.
+ // => Find the SPS nal header
+ const nalHeader = /00000001[0-9]7/.exec(codecPrivateData);
+ if (!nalHeader.length) {
+ return '';
+ }
+ if (!codecPrivateData) {
+ return '';
+ }
+ // => Find the 6 characters after the SPS nalHeader (if it exists)
+ const avcoti = codecPrivateData.substr(
+ codecPrivateData.indexOf(nalHeader[0]) + 10, 6);
+
+ return 'avc1.' + avcoti;
+ }
+
+ /**
+ * @param {!shaka.media.InitSegmentReference} initSegmentRef
+ * @param {!shaka.extern.Stream} stream
+ * @param {!Element} streamIndex
+ * @param {!Array.} timeline
+ * @param {!shaka.mss.MssParser.Context} context
+ * @return {!Array.}
+ * @private
+ */
+ createSegments_(initSegmentRef, stream, streamIndex, timeline, context) {
+ const ManifestParserUtils = shaka.util.ManifestParserUtils;
+ const url = streamIndex.getAttribute('Url');
+ goog.asserts.assert(url, 'Missing URL for segments');
+
+ const mediaUrl = url.replace('{bitrate}', String(stream.bandwidth));
+
+ const segments = [];
+ for (const time of timeline) {
+ const getUris = () => {
+ return ManifestParserUtils.resolveUris(this.manifestUris_,
+ [mediaUrl.replace('{start time}', String(time.unscaledStart))]);
+ };
+ segments.push(new shaka.media.SegmentReference(
+ time.start,
+ time.end,
+ getUris,
+ /* startByte= */ 0,
+ /* endByte= */ null,
+ initSegmentRef,
+ /* timestampOffset= */ 0,
+ /* appendWindowStart= */ 0,
+ /* appendWindowEnd= */ context.duration));
+ }
+ return segments;
+ }
+
+ /**
+ * Expands a streamIndex into an array-based timeline. The results are in
+ * seconds.
+ *
+ * @param {!Element} streamIndex
+ * @param {number} timescale
+ * @param {number} duration The duration in seconds.
+ * @return {!Array.}
+ * @private
+ */
+ createTimeline_(streamIndex, timescale, duration) {
+ goog.asserts.assert(
+ timescale > 0 && timescale < Infinity,
+ 'timescale must be a positive, finite integer');
+ goog.asserts.assert(
+ duration > 0, 'duration must be a positive integer');
+
+ const XmlUtils = shaka.util.XmlUtils;
+
+ const timePoints = XmlUtils.findChildren(streamIndex, 'c');
+
+ /** @type {!Array.} */
+ const timeline = [];
+ let lastEndTime = 0;
+
+ for (let i = 0; i < timePoints.length; ++i) {
+ const timePoint = timePoints[i];
+ const next = timePoints[i + 1];
+ const t =
+ XmlUtils.parseAttr(timePoint, 't', XmlUtils.parseNonNegativeInt);
+ const d =
+ XmlUtils.parseAttr(timePoint, 'd', XmlUtils.parseNonNegativeInt);
+ const r = XmlUtils.parseAttr(timePoint, 'r', XmlUtils.parseInt);
+
+ if (!d) {
+ shaka.log.warning(
+ '"c" element must have a duration:',
+ 'ignoring the remaining "c" elements.', timePoint);
+ return timeline;
+ }
+
+ let startTime = t != null ? t : lastEndTime;
+
+ let repeat = r || 0;
+ if (repeat < 0) {
+ if (next) {
+ const nextStartTime =
+ XmlUtils.parseAttr(next, 't', XmlUtils.parseNonNegativeInt);
+ if (nextStartTime == null) {
+ shaka.log.warning(
+ 'An "c" element cannot have a negative repeat',
+ 'if the next "c" element does not have a valid start time:',
+ 'ignoring the remaining "c" elements.', timePoint);
+ return timeline;
+ } else if (startTime >= nextStartTime) {
+ shaka.log.warning(
+ 'An "c" element cannot have a negative repeatif its start ',
+ 'time exceeds the next "c" element\'s start time:',
+ 'ignoring the remaining "c" elements.', timePoint);
+ return timeline;
+ }
+ repeat = Math.ceil((nextStartTime - startTime) / d) - 1;
+ } else {
+ if (duration == Infinity) {
+ // The MSS spec. actually allows the last "c" element to have a
+ // negative repeat value even when it has an infinite
+ // duration. No one uses this feature and no one ever should,
+ // ever.
+ shaka.log.warning(
+ 'The last "c" element cannot have a negative repeat',
+ 'if the Period has an infinite duration:',
+ 'ignoring the last "c" element.', timePoint);
+ return timeline;
+ } else if (startTime / timescale >= duration) {
+ shaka.log.warning(
+ 'The last "c" element cannot have a negative repeat',
+ 'if its start time exceeds the duration:',
+ 'igoring the last "c" element.', timePoint);
+ return timeline;
+ }
+ repeat = Math.ceil((duration * timescale - startTime) / d) - 1;
+ }
+ }
+
+ for (let j = 0; j <= repeat; ++j) {
+ const endTime = startTime + d;
+ const item = {
+ start: startTime / timescale,
+ end: endTime / timescale,
+ unscaledStart: startTime,
+ };
+ timeline.push(item);
+
+ startTime = endTime;
+ lastEndTime = endTime;
+ }
+ }
+
+ return timeline;
+ }
+
+ /**
+ * @param {?shaka.extern.Stream} audioStream
+ * @param {?shaka.extern.Stream} videoStream
+ * @return {!shaka.extern.Variant}
+ * @private
+ */
+ createVariant_(audioStream, videoStream) {
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
+
+ goog.asserts.assert(!audioStream ||
+ audioStream.type == ContentType.AUDIO, 'Audio parameter mismatch!');
+ goog.asserts.assert(!videoStream ||
+ videoStream.type == ContentType.VIDEO, 'Video parameter mismatch!');
+
+ let bandwidth = 0;
+ if (audioStream && audioStream.bandwidth && audioStream.bandwidth > 0) {
+ bandwidth += audioStream.bandwidth;
+ }
+ if (videoStream && videoStream.bandwidth && videoStream.bandwidth > 0) {
+ bandwidth += videoStream.bandwidth;
+ }
+
+ return {
+ id: this.globalId_++,
+ language: audioStream ? audioStream.language : 'und',
+ disabledUntilTime: 0,
+ primary: (!!audioStream && audioStream.primary) ||
+ (!!videoStream && videoStream.primary),
+ audio: audioStream,
+ video: videoStream,
+ bandwidth: bandwidth,
+ allowedByApplication: true,
+ allowedByKeySystem: true,
+ decodingInfos: [],
+ };
+ }
+};
+
+
+/**
+ * Contains the minimum amount of time, in seconds, between manifest update
+ * requests.
+ *
+ * @private
+ * @const {number}
+ */
+shaka.mss.MssParser.MIN_UPDATE_PERIOD_ = 3;
+
+
+/**
+ * @private
+ * @const {number}
+ */
+shaka.mss.MssParser.DEFAULT_TIME_SCALE_ = 1e7;
+
+
+/**
+ * MSS supported codecs.
+ *
+ * @private
+ * @const {!Array.}
+ */
+shaka.mss.MssParser.SUPPORTED_CODECS_ = [
+ 'AAC',
+ 'AACL',
+ 'AACH',
+ 'AACP',
+ 'AVC1',
+ 'H264',
+ 'TTML',
+ 'DFXP',
+];
+
+
+/**
+ * MPEG-DASH Role and accessibility mapping for text tracks according to
+ * ETSI TS 103 285 v1.1.1 (section 7.1.2)
+ *
+ * @const {!Object.}
+ * @private
+ */
+shaka.mss.MssParser.ROLE_MAPPING_ = {
+ 'CAPT': 'main',
+ 'SUBT': 'alternate',
+ 'DESC': 'main',
+};
+
+
+/**
+ * @typedef {{
+ * variants: !Array.,
+ * textStreams: !Array.,
+ * timescale: number,
+ * duration: number
+ * }}
+ *
+ * @property {!Array.} variants
+ * The presentation's Variants.
+ * @property {!Array.} textStreams
+ * The presentation's text streams.
+ * @property {number} timescale
+ * The presentation's timescale.
+ * @property {number} duration
+ * The presentation's duration.
+ */
+shaka.mss.MssParser.Context;
+
+
+/**
+ * @typedef {{
+ * start: number,
+ * unscaledStart: number,
+ * end: number
+ * }}
+ *
+ * @description
+ * Defines a time range of a media segment. Times are in seconds.
+ *
+ * @property {number} start
+ * The start time of the range.
+ * @property {number} unscaledStart
+ * The start time of the range in representation timescale units.
+ * @property {number} end
+ * The end time (exclusive) of the range.
+ */
+shaka.mss.MssParser.TimeRange;
+
+if (shaka.dependencies.isoBoxer()) {
+ shaka.media.ManifestParser.registerParserByExtension(
+ 'ism', () => new shaka.mss.MssParser());
+ shaka.media.ManifestParser.registerParserByMime(
+ 'application/vnd.ms-sstr+xml', () => new shaka.mss.MssParser());
+}
diff --git a/lib/mss/mss_utils.js b/lib/mss/mss_utils.js
new file mode 100644
index 0000000000..f753f55bfd
--- /dev/null
+++ b/lib/mss/mss_utils.js
@@ -0,0 +1,826 @@
+/*! @license
+ * MSS Utils
+ * Copyright 2015 Dash Industry Forum
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/*
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the Dash Industry Forum nor the names of its
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS”
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+goog.provide('shaka.mss.MssUtils');
+
+goog.require('goog.asserts');
+goog.require('shaka.util.BufferUtils');
+goog.require('shaka.util.Error');
+goog.require('shaka.util.ManifestParserUtils');
+goog.require('shaka.dependencies');
+
+
+/**
+ * @summary MSS processing utility functions.
+ */
+shaka.mss.MssUtils = class {
+ /**
+ * Generate a Init Segment (MP4) for a MSS stream.
+ *
+ * @param {shaka.extern.Stream} stream
+ * @return {!BufferSource}
+ */
+ static generateInitSegment(stream) {
+ const MssUtils = shaka.mss.MssUtils;
+ const isoBoxer = shaka.dependencies.isoBoxer();
+ goog.asserts.assert(isoBoxer, 'ISOBoxer should be defined.');
+ const isoFile = isoBoxer.createFile();
+ MssUtils.createFtypBox_(isoBoxer, isoFile);
+ MssUtils.createMoovBox_(isoBoxer, isoFile, stream);
+ return shaka.util.BufferUtils.toUint8(isoFile.write());
+ }
+
+ /**
+ * Create ftyp box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} isoFile
+ * @private
+ */
+ static createFtypBox_(isoBoxer, isoFile) {
+ const ftyp = isoBoxer.createBox('ftyp', isoFile);
+ ftyp.major_brand = 'iso6';
+ // is an informative integer for the minor version of the major brand
+ ftyp.minor_version = 1;
+ // is a list, to the end of the box, of brands isom, iso6 and msdh
+ ftyp.compatible_brands = [];
+ // => decimal ASCII value for isom
+ ftyp.compatible_brands[0] = 'isom';
+ // => decimal ASCII value for iso6
+ ftyp.compatible_brands[1] = 'iso6';
+ // => decimal ASCII value for msdh
+ ftyp.compatible_brands[2] = 'msdh';
+ }
+
+ /**
+ * Create moov box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} isoFile
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createMoovBox_(isoBoxer, isoFile, stream) {
+ const MssUtils = shaka.mss.MssUtils;
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
+ // moov box
+ const moov = isoBoxer.createBox('moov', isoFile);
+ // moov/mvhd
+ MssUtils.createMvhdBox_(isoBoxer, moov, stream);
+ // moov/trak
+ const trak = isoBoxer.createBox('trak', moov);
+ // moov/trak/tkhd
+ MssUtils.createTkhdBox_(isoBoxer, trak, stream);
+ // moov/trak/mdia
+ const mdia = isoBoxer.createBox('mdia', trak);
+ // moov/trak/mdia/mdhd
+ MssUtils.createMdhdBox_(isoBoxer, mdia, stream);
+ // moov/trak/mdia/hdlr
+ MssUtils.createHdlrBox_(isoBoxer, mdia, stream);
+ // moov/trak/mdia/minf
+ const minf = isoBoxer.createBox('minf', mdia);
+ switch (stream.type) {
+ case ContentType.VIDEO:
+ // moov/trak/mdia/minf/vmhd
+ MssUtils.createVmhdBox_(isoBoxer, minf);
+ break;
+ case ContentType.AUDIO:
+ // moov/trak/mdia/minf/smhd
+ MssUtils.createSmhdBox_(isoBoxer, minf);
+ break;
+ }
+ // moov/trak/mdia/minf/dinf
+ const dinf = isoBoxer.createBox('dinf', minf);
+ // moov/trak/mdia/minf/dinf/dref
+ MssUtils.createDrefBox_(isoBoxer, dinf);
+ // moov/trak/mdia/minf/stbl
+ const stbl = isoBoxer.createBox('stbl', minf);
+ // Create empty stts, stsc, stco and stsz boxes
+ // Use data field as for codem-isoboxer unknown boxes for setting
+ // fields value
+ // moov/trak/mdia/minf/stbl/stts
+ const stts = isoBoxer.createFullBox('stts', stbl);
+ // version = 0, flags = 0, entry_count = 0
+ stts._data = [0, 0, 0, 0, 0, 0, 0, 0];
+ // moov/trak/mdia/minf/stbl/stsc
+ const stsc = isoBoxer.createFullBox('stsc', stbl);
+ // version = 0, flags = 0, entry_count = 0
+ stsc._data = [0, 0, 0, 0, 0, 0, 0, 0];
+ // moov/trak/mdia/minf/stbl/stco
+ const stco = isoBoxer.createFullBox('stco', stbl);
+ // version = 0, flags = 0, entry_count = 0
+ stco._data = [0, 0, 0, 0, 0, 0, 0, 0];
+ // moov/trak/mdia/minf/stbl/stsz
+ const stsz = isoBoxer.createFullBox('stsz', stbl);
+ // version = 0, flags = 0, sample_size = 0, sample_count = 0
+ stsz._data = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ // moov/trak/mdia/minf/stbl/stsd
+ MssUtils.createStsdBox_(isoBoxer, stbl, stream);
+ // moov/mvex
+ const mvex = isoBoxer.createBox('mvex', moov);
+ // moov/mvex/trex
+ MssUtils.createTrexBox_(isoBoxer, mvex, stream);
+ if (stream.encrypted) {
+ MssUtils.createProtectionSystemSpecificHeaderBox_(isoBoxer, moov, stream);
+ }
+ }
+
+ /**
+ * Create mvhd box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} moov
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createMvhdBox_(isoBoxer, moov, stream) {
+ const mvhd = isoBoxer.createFullBox('mvhd', moov);
+ // version = 1 in order to have 64bits duration value
+ mvhd.version = 1;
+ // the creation time of the presentation => ignore (set to 0)
+ mvhd.creation_time = 0;
+ // the most recent time the presentation was modified => ignore (set to 0)
+ mvhd.modification_time = 0;
+ // the time-scale for the entire presentation => 10000000 for MSS
+ const timescale = stream.mssPrivateData.timescale;
+ mvhd.timescale = timescale;
+ // the length of the presentation (in the indicated timescale)
+ const duration = stream.mssPrivateData.duration;
+ mvhd.duration = duration === Infinity ?
+ 0x1FFFFFFFFFFFFF : Math.round(duration * timescale);
+ // 16.16 number, '1.0' = normal playback
+ mvhd.rate = 1.0;
+ // 8.8 number, '1.0' = full volume
+ mvhd.volume = 1.0;
+ mvhd.reserved1 = 0;
+ mvhd.reserved2 = [0x0, 0x0];
+ mvhd.matrix = [
+ 1, 0, 0, // provides a transformation matrix for the video;
+ 0, 1, 0, // (u,v,w) are restricted here to (0,0,1)
+ 0, 0, 16384,
+ ];
+ mvhd.pre_defined = [0, 0, 0, 0, 0, 0];
+ // indicates a value to use for the track ID of the next track to be
+ // added to this presentation
+ mvhd.next_track_ID = (stream.id + 1) + 1;
+ }
+
+ /**
+ * Create tkhd box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} trak
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createTkhdBox_(isoBoxer, trak, stream) {
+ const tkhd = isoBoxer.createFullBox('tkhd', trak);
+ // version = 1 in order to have 64bits duration value
+ tkhd.version = 1;
+ // Track_enabled (0x000001): Indicates that the track is enabled
+ // Track_in_movie (0x000002): Indicates that the track is used in
+ // the presentation
+ // Track_in_preview (0x000004): Indicates that the track is used when
+ // previewing the presentation
+ tkhd.flags = 0x1 | 0x2 | 0x4;
+ // the creation time of the presentation => ignore (set to 0)
+ tkhd.creation_time = 0;
+ // the most recent time the presentation was modified => ignore (set to 0)
+ tkhd.modification_time = 0;
+ // uniquely identifies this track over the entire life-time of this
+ // presentation
+ tkhd.track_ID = (stream.id + 1);
+ tkhd.reserved1 = 0;
+ // the duration of this track (in the timescale indicated in the Movie
+ // Header Box)
+ const duration = stream.mssPrivateData.duration;
+ const timescale = stream.mssPrivateData.timescale;
+ tkhd.duration = duration === Infinity ?
+ 0x1FFFFFFFFFFFFF : Math.round(duration * timescale);
+ tkhd.reserved2 = [0x0, 0x0];
+ // specifies the front-to-back ordering of video tracks; tracks with lower
+ // numbers are closer to the viewer => 0 since only one video track
+ tkhd.layer = 0;
+ // specifies a group or collection of tracks => ignore
+ tkhd.alternate_group = 0;
+ // '1.0' = full volume
+ tkhd.volume = 1.0;
+ tkhd.reserved3 = 0;
+ tkhd.matrix = [
+ 1, 0, 0, // provides a transformation matrix for the video;
+ 0, 1, 0, // (u,v,w) are restricted here to (0,0,1)
+ 0, 0, 16384,
+ ];
+ // visual presentation width
+ tkhd.width = stream.width;
+ // visual presentation height
+ tkhd.height = stream.height;
+ }
+
+ /**
+ * Create mdhd box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} mdia
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createMdhdBox_(isoBoxer, mdia, stream) {
+ const mdhd = isoBoxer.createFullBox('mdhd', mdia);
+ // version = 1 in order to have 64bits duration value
+ mdhd.version = 1;
+ // the creation time of the presentation => ignore (set to 0)
+ mdhd.creation_time = 0;
+ // the most recent time the presentation was modified => ignore (set to 0)
+ mdhd.modification_time = 0;
+ // the time-scale for the entire presentation
+ const timescale = stream.mssPrivateData.timescale;
+ mdhd.timescale = timescale;
+ // the duration of this media (in the scale of the timescale).
+ // If the duration cannot be determined then duration is set to all 1s.
+ const duration = stream.mssPrivateData.duration;
+ mdhd.duration = duration === Infinity ?
+ 0x1FFFFFFFFFFFFF : Math.round(duration * timescale);
+ // declares the language code for this media
+ mdhd.language = stream.language;
+ mdhd.pre_defined = 0;
+ }
+
+ /**
+ * Create hdlr box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} mdia
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createHdlrBox_(isoBoxer, mdia, stream) {
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
+ const hdlr = isoBoxer.createFullBox('hdlr', mdia);
+ hdlr.pre_defined = 0;
+ switch (stream.type) {
+ case ContentType.VIDEO:
+ hdlr.handler_type = 'vide';
+ break;
+ case ContentType.AUDIO:
+ hdlr.handler_type = 'soun';
+ break;
+ default:
+ hdlr.handler_type = 'meta';
+ break;
+ }
+ hdlr.name = stream.originalId;
+ hdlr.reserved = [0, 0, 0];
+ }
+
+ /**
+ * Create vmhd box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} minf
+ * @private
+ */
+ static createVmhdBox_(isoBoxer, minf) {
+ const vmhd = isoBoxer.createFullBox('vmhd', minf);
+ vmhd.flags = 1;
+ // specifies a composition mode for this video track, from the following
+ // enumerated set, which may be extended by derived specifications:
+ // copy = 0 copy over the existing image
+ vmhd.graphicsmode = 0;
+ // is a set of 3 colour values (red, green, blue) available for use by
+ // graphics modes
+ vmhd.opcolor = [0, 0, 0];
+ }
+
+ /**
+ * Create smhd box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} minf
+ * @private
+ */
+ static createSmhdBox_(isoBoxer, minf) {
+ const smhd = isoBoxer.createFullBox('smhd', minf);
+ smhd.flags = 1;
+ // is a fixed-point 8.8 number that places mono audio tracks in a stereo
+ // space; 0 is centre (the normal value); full left is -1.0 and full
+ // right is 1.0.
+ smhd.balance = 0;
+ smhd.reserved = 0;
+ }
+
+ /**
+ * Create dref box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} dinf
+ * @private
+ */
+ static createDrefBox_(isoBoxer, dinf) {
+ const dref = isoBoxer.createFullBox('dref', dinf);
+ dref.entry_count = 1;
+ dref.entries = [];
+ const url = isoBoxer.createFullBox('url ', dref, false);
+ url.location = '';
+ url.flags = 1;
+ dref.entries.push(url);
+ }
+
+ /**
+ * Create stsd box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} stbl
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createStsdBox_(isoBoxer, stbl, stream) {
+ const MssUtils = shaka.mss.MssUtils;
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
+ const stsd = isoBoxer.createFullBox('stsd', stbl);
+ stsd.entries = [];
+ switch (stream.type) {
+ case ContentType.VIDEO:
+ case ContentType.AUDIO:
+ stsd.entries.push(MssUtils.createSampleEntry_(isoBoxer, stsd, stream));
+ break;
+ default:
+ break;
+ }
+ // is an integer that counts the actual entries
+ stsd.entry_count = stsd.entries.length;
+ }
+
+ /**
+ * Create sample entry box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} stsd
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createSampleEntry_(isoBoxer, stsd, stream) {
+ const MssUtils = shaka.mss.MssUtils;
+ const codec = stream.codecs.substring(0, stream.codecs.indexOf('.'));
+ switch (codec) {
+ case 'avc1':
+ return MssUtils.createAVCVisualSampleEntry_(
+ isoBoxer, stsd, codec, stream);
+ case 'mp4a':
+ return MssUtils.createMP4AudioSampleEntry_(
+ isoBoxer, stsd, codec, stream);
+ default:
+ throw new shaka.util.Error(
+ shaka.util.Error.Severity.CRITICAL,
+ shaka.util.Error.Category.MEDIA,
+ shaka.util.Error.Code.MSS_TRANSMUXING_CODEC_UNKNOWN,
+ codec);
+ }
+ }
+
+ /**
+ * Create AVC Visual Sample Entry box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} stsd
+ * @param {string} codec
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createAVCVisualSampleEntry_(isoBoxer, stsd, codec, stream) {
+ const MssUtils = shaka.mss.MssUtils;
+ let avc1;
+ if (stream.encrypted) {
+ avc1 = isoBoxer.createBox('encv', stsd, false);
+ } else {
+ avc1 = isoBoxer.createBox('avc1', stsd, false);
+ }
+ // SampleEntry fields
+ avc1.reserved1 = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0];
+ avc1.data_reference_index = 1;
+ // VisualSampleEntry fields
+ avc1.pre_defined1 = 0;
+ avc1.reserved2 = 0;
+ avc1.pre_defined2 = [0, 0, 0];
+ avc1.height = stream.height;
+ avc1.width = stream.width;
+ // 72 dpi
+ avc1.horizresolution = 72;
+ // 72 dpi
+ avc1.vertresolution = 72;
+ avc1.reserved3 = 0;
+ // 1 compressed video frame per sample
+ avc1.frame_count = 1;
+ avc1.compressorname = [
+ 0x0A, 0x41, 0x56, 0x43, 0x20, 0x43, 0x6F, 0x64, // = 'AVC Coding';
+ 0x69, 0x6E, 0x67, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ ];
+ // 0x0018 – images are in colour with no alpha.
+ avc1.depth = 0x0018;
+ avc1.pre_defined3 = 65535;
+ avc1.config = MssUtils.createAVC1ConfigurationRecord_(isoBoxer, stream);
+ if (stream.encrypted) {
+ // Create and add Protection Scheme Info Box
+ const sinf = isoBoxer.createBox('sinf', avc1);
+ // Create and add Original Format Box => indicate codec type of the
+ // encrypted content
+ MssUtils.createOriginalFormatBox_(isoBoxer, sinf, codec);
+ // Create and add Scheme Type box
+ MssUtils.createSchemeTypeBox_(isoBoxer, sinf);
+ // Create and add Scheme Information Box
+ MssUtils.createSchemeInformationBox_(isoBoxer, sinf, stream);
+ }
+ return avc1;
+ }
+
+ /**
+ * Create AVC1 configuration record.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createAVC1ConfigurationRecord_(isoBoxer, stream) {
+ const MssUtils = shaka.mss.MssUtils;
+
+ const NALUTYPE_SPS = 7;
+ const NALUTYPE_PPS = 8;
+
+ // length = 15 by default (0 SPS and 0 PPS)
+ let avcCLength = 15;
+ // First get all SPS and PPS from codecPrivateData
+ const sps = [];
+ const pps = [];
+ let AVCProfileIndication = 0;
+ let AVCLevelIndication = 0;
+ let profileCompatibility = 0;
+ const codecPrivateData = stream.mssPrivateData.codecPrivateData;
+ const nalus = codecPrivateData.split('00000001').slice(1);
+ for (let i = 0; i < nalus.length; i++) {
+ const naluBytes = MssUtils.hexStringToBuffer_(nalus[i]);
+ const naluType = naluBytes[0] & 0x1F;
+ switch (naluType) {
+ case NALUTYPE_SPS:
+ sps.push(naluBytes);
+ // 2 = sequenceParameterSetLength field length
+ avcCLength += naluBytes.length + 2;
+ break;
+ case NALUTYPE_PPS:
+ pps.push(naluBytes);
+ // 2 = pictureParameterSetLength field length
+ avcCLength += naluBytes.length + 2;
+ break;
+ default:
+ break;
+ }
+ }
+ // Get profile and level from SPS
+ if (sps.length > 0) {
+ AVCProfileIndication = sps[0][1];
+ profileCompatibility = sps[0][2];
+ AVCLevelIndication = sps[0][3];
+ }
+ // Generate avcC buffer
+ const avcC = new Uint8Array(avcCLength);
+ let i = 0;
+ // length
+ avcC[i++] = (avcCLength & 0xFF000000) >> 24;
+ avcC[i++] = (avcCLength & 0x00FF0000) >> 16;
+ avcC[i++] = (avcCLength & 0x0000FF00) >> 8;
+ avcC[i++] = (avcCLength & 0x000000FF);
+ // type = 'avcC'
+ avcC.set([0x61, 0x76, 0x63, 0x43], i);
+ i += 4;
+ // configurationVersion = 1
+ avcC[i++] = 1;
+ avcC[i++] = AVCProfileIndication;
+ avcC[i++] = profileCompatibility;
+ avcC[i++] = AVCLevelIndication;
+ // '11111' + lengthSizeMinusOne = 3
+ avcC[i++] = 0xFF;
+ // '111' + numOfSequenceParameterSets
+ avcC[i++] = 0xE0 | sps.length;
+ for (let n = 0; n < sps.length; n++) {
+ avcC[i++] = (sps[n].length & 0xFF00) >> 8;
+ avcC[i++] = (sps[n].length & 0x00FF);
+ avcC.set(sps[n], i);
+ i += sps[n].length;
+ }
+ // numOfPictureParameterSets
+ avcC[i++] = pps.length;
+ for (let n = 0; n < pps.length; n++) {
+ avcC[i++] = (pps[n].length & 0xFF00) >> 8;
+ avcC[i++] = (pps[n].length & 0x00FF);
+ avcC.set(pps[n], i);
+ i += pps[n].length;
+ }
+ return avcC;
+ }
+
+ /**
+ * Create MP4 Audio Sample Entry box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} stsd
+ * @param {string} codec
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createMP4AudioSampleEntry_(isoBoxer, stsd, codec, stream) {
+ const MssUtils = shaka.mss.MssUtils;
+ // By default assumes stereo
+ const channelsCount = stream.channelsCount || 2;
+ // By default assumes 44.1khz
+ const audioSamplingRate = stream.audioSamplingRate || 44100;
+ let mp4a;
+ if (stream.encrypted) {
+ mp4a = isoBoxer.createBox('enca', stsd, false);
+ } else {
+ mp4a = isoBoxer.createBox('mp4a', stsd, false);
+ }
+ // SampleEntry fields
+ mp4a.reserved1 = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0];
+ mp4a.data_reference_index = 1;
+ // AudioSampleEntry fields
+ mp4a.reserved2 = [0x0, 0x0];
+ mp4a.channelcount = channelsCount;
+ mp4a.samplesize = 16;
+ mp4a.pre_defined = 0;
+ mp4a.reserved_3 = 0;
+ mp4a.samplerate = audioSamplingRate << 16;
+ mp4a.esds = MssUtils.createMPEG4AACESDescriptor_(isoBoxer, stream);
+ if (stream.encrypted) {
+ // Create and add Protection Scheme Info Box
+ const sinf = isoBoxer.createBox('sinf', mp4a);
+ // Create and add Original Format Box => indicate codec type of the
+ // encrypted content
+ MssUtils.createOriginalFormatBox_(isoBoxer, sinf, codec);
+ // Create and add Scheme Type box
+ MssUtils.createSchemeTypeBox_(isoBoxer, sinf);
+ // Create and add Scheme Information Box
+ MssUtils.createSchemeInformationBox_(isoBoxer, sinf, stream);
+ }
+ return mp4a;
+ }
+
+ /**
+ * Create ESDS descriptor.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createMPEG4AACESDescriptor_(isoBoxer, stream) {
+ const MssUtils = shaka.mss.MssUtils;
+ const codecPrivateData = stream.mssPrivateData.codecPrivateData;
+ goog.asserts.assert(codecPrivateData, 'Missing CodecPrivateData');
+ // AudioSpecificConfig (see ISO/IEC 14496-3, subpart 1) => corresponds to
+ // hex bytes contained in 'codecPrivateData' field
+ const audioSpecificConfig = MssUtils.hexStringToBuffer_(codecPrivateData);
+
+ // ESDS length = esds box header length (= 12) +
+ // ES_Descriptor header length (= 5) +
+ // DecoderConfigDescriptor header length (= 15) +
+ // decoderSpecificInfo header length (= 2) +
+ // AudioSpecificConfig length (= codecPrivateData length)
+ const esdsLength = 34 + audioSpecificConfig.length;
+ const esds = new Uint8Array(esdsLength);
+ let i = 0;
+ // esds box
+ // esds box length
+ esds[i++] = (esdsLength & 0xFF000000) >> 24;
+ esds[i++] = (esdsLength & 0x00FF0000) >> 16;
+ esds[i++] = (esdsLength & 0x0000FF00) >> 8;
+ esds[i++] = (esdsLength & 0x000000FF);
+ // type = 'esds'
+ esds.set([0x65, 0x73, 0x64, 0x73], i);
+ i += 4;
+ // version = 0, flags = 0
+ esds.set([0, 0, 0, 0], i);
+ i += 4;
+ // ES_Descriptor (see ISO/IEC 14496-1 (Systems))
+ // tag = 0x03 (ES_DescrTag)
+ esds[i++] = 0x03;
+ // size
+ esds[i++] = 20 + audioSpecificConfig.length;
+ // ES_ID = track_id
+ esds[i++] = ((stream.id + 1) & 0xFF00) >> 8;
+ esds[i++] = ((stream.id + 1) & 0x00FF);
+ // flags and streamPriority
+ esds[i++] = 0;
+ // DecoderConfigDescriptor (see ISO/IEC 14496-1 (Systems))
+ // tag = 0x04 (DecoderConfigDescrTag)
+ esds[i++] = 0x04;
+ // size
+ esds[i++] = 15 + audioSpecificConfig.length;
+ // objectTypeIndication = 0x40 (MPEG-4 AAC)
+ esds[i++] = 0x40;
+ // streamType = 0x05 (Audiostream)
+ esds[i] = 0x05 << 2;
+ // upStream = 0
+ esds[i] |= 0 << 1;
+ // reserved = 1
+ esds[i++] |= 1;
+ // buffersizeDB = undefined
+ esds[i++] = 0xFF;
+ esds[i++] = 0xFF;
+ esds[i++] = 0xFF;
+ const bandwidth = stream.bandwidth || 0;
+ // maxBitrate
+ esds[i++] = (bandwidth & 0xFF000000) >> 24;
+ esds[i++] = (bandwidth & 0x00FF0000) >> 16;
+ esds[i++] = (bandwidth & 0x0000FF00) >> 8;
+ esds[i++] = (bandwidth & 0x000000FF);
+ // avgbitrate
+ esds[i++] = (bandwidth & 0xFF000000) >> 24;
+ esds[i++] = (bandwidth & 0x00FF0000) >> 16;
+ esds[i++] = (bandwidth & 0x0000FF00) >> 8;
+ esds[i++] = (bandwidth & 0x000000FF);
+
+ // DecoderSpecificInfo (see ISO/IEC 14496-1 (Systems))
+ // tag = 0x05 (DecSpecificInfoTag)
+ esds[i++] = 0x05;
+ // size
+ esds[i++] = audioSpecificConfig.length;
+ // AudioSpecificConfig bytes
+ esds.set(audioSpecificConfig, i);
+
+ return esds;
+ }
+
+ /**
+ * Create frma box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} sinf
+ * @param {string} codec
+ * @private
+ */
+ static createOriginalFormatBox_(isoBoxer, sinf, codec) {
+ const MssUtils = shaka.mss.MssUtils;
+ const frma = isoBoxer.createBox('frma', sinf);
+ frma.data_format = MssUtils.stringToCharCode_(codec);
+ }
+
+ /**
+ * Create schm box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} sinf
+ * @private
+ */
+ static createSchemeTypeBox_(isoBoxer, sinf) {
+ const schm = isoBoxer.createFullBox('schm', sinf);
+ schm.flags = 0;
+ schm.version = 0;
+ // 'cenc' => common encryption
+ schm.scheme_type = 0x63656E63;
+ // version set to 0x00010000 (Major version 1, Minor version 0)
+ schm.scheme_version = 0x00010000;
+ }
+
+ /**
+ * Create schi box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} sinf
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createSchemeInformationBox_(isoBoxer, sinf, stream) {
+ const MssUtils = shaka.mss.MssUtils;
+ const schi = isoBoxer.createBox('schi', sinf);
+ // Create and add Track Encryption Box
+ MssUtils.createTrackEncryptionBox_(isoBoxer, schi, stream);
+ }
+
+ /**
+ * Create tenc box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} schi
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createTrackEncryptionBox_(isoBoxer, schi, stream) {
+ const tenc = isoBoxer.createFullBox('tenc', schi);
+ tenc.flags = 0;
+ tenc.version = 0;
+ tenc.default_IsEncrypted = 0x1;
+ tenc.default_IV_size = 8;
+ let defaultKID = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0];
+ for (const drmInfo of stream.drmInfos) {
+ if (drmInfo && drmInfo.keyId && drmInfo.keyIds.size) {
+ for (const keyId of drmInfo.keyIds) {
+ defaultKID = keyId;
+ }
+ }
+ }
+ tenc.default_KID = defaultKID;
+ }
+
+ /**
+ * Create trex box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} moov
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createTrexBox_(isoBoxer, moov, stream) {
+ const trex = isoBoxer.createFullBox('trex', moov);
+ trex.track_ID = (stream.id + 1);
+ trex.default_sample_description_index = 1;
+ trex.default_sample_duration = 0;
+ trex.default_sample_size = 0;
+ trex.default_sample_flags = 0;
+ }
+
+ /**
+ * Create PSSH box.
+ *
+ * @param {ISOBoxer} isoBoxer
+ * @param {ISOBoxer} moov
+ * @param {shaka.extern.Stream} stream
+ * @private
+ */
+ static createProtectionSystemSpecificHeaderBox_(isoBoxer, moov, stream) {
+ const BufferUtils = shaka.util.BufferUtils;
+ for (const drmInfo of stream.drmInfos) {
+ if (!drmInfo.initData) {
+ continue;
+ }
+ for (const initData of drmInfo.initData) {
+ const initDataBuffer = BufferUtils.toArrayBuffer(initData.initData);
+ const parsedBuffer = isoBoxer.parseBuffer(initDataBuffer);
+ const pssh = parsedBuffer.fetch('pssh');
+ if (pssh) {
+ isoBoxer.Utils.appendBox(moov, pssh);
+ }
+ }
+ }
+ }
+
+ /**
+ * Convert a hex string to buffer.
+ *
+ * @param {string} str
+ * @return {Uint8Array}
+ * @private
+ */
+ static hexStringToBuffer_(str) {
+ const buf = new Uint8Array(str.length / 2);
+ for (let i = 0; i < str.length / 2; i += 1) {
+ buf[i] = parseInt(String(str[i * 2] + str[i * 2 + 1]), 16);
+ }
+ return buf;
+ }
+
+ /**
+ * Convert a string to char code.
+ *
+ * @param {string} str
+ * @return {number}
+ * @private
+ */
+ static stringToCharCode_(str) {
+ let code = 0;
+ for (let i = 0; i < str.length; i += 1) {
+ code |= str.charCodeAt(i) << ((str.length - i - 1) * 8);
+ }
+ return code;
+ }
+};
+
diff --git a/lib/transmuxer/mss_transmuxer.js b/lib/transmuxer/mss_transmuxer.js
new file mode 100644
index 0000000000..fe684e6f2e
--- /dev/null
+++ b/lib/transmuxer/mss_transmuxer.js
@@ -0,0 +1,355 @@
+/*! @license
+ * MSS Transmuxer
+ * Copyright 2015 Dash Industry Forum
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/*
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the Dash Industry Forum nor the names of its
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS”
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+goog.provide('shaka.transmuxer.MssTransmuxer');
+
+goog.require('shaka.media.Capabilities');
+goog.require('shaka.transmuxer.TransmuxerEngine');
+goog.require('shaka.util.BufferUtils');
+goog.require('shaka.util.Error');
+goog.require('shaka.util.ManifestParserUtils');
+goog.require('shaka.dependencies');
+
+goog.requireType('shaka.media.SegmentReference');
+
+
+/**
+ * @implements {shaka.extern.Transmuxer}
+ * @export
+ */
+shaka.transmuxer.MssTransmuxer = class {
+ /**
+ * @param {string} mimeType
+ */
+ constructor(mimeType) {
+ /** @private {string} */
+ this.originalMimeType_ = mimeType;
+
+ /** @private {?ISOBoxer} */
+ this.isoBoxer_ = shaka.dependencies.isoBoxer();
+
+ if (this.isoBoxer_) {
+ this.addSpecificBoxProcessor_();
+ }
+ }
+
+ /**
+ * Add specific box processor for codem-isoboxer
+ *
+ * @private
+ */
+ addSpecificBoxProcessor_() {
+ // eslint-disable-next-line no-restricted-syntax
+ this.isoBoxer_.addBoxProcessor('saio', function() {
+ // eslint-disable-next-line no-invalid-this
+ const box = /** @type {!ISOBox} */(this);
+ box._procFullBox();
+ if (box.flags & 1) {
+ box._procField('aux_info_type', 'uint', 32);
+ box._procField('aux_info_type_parameter', 'uint', 32);
+ }
+ box._procField('entry_count', 'uint', 32);
+ box._procFieldArray('offset', box.entry_count, 'uint',
+ (box.version === 1) ? 64 : 32);
+ });
+ // eslint-disable-next-line no-restricted-syntax
+ this.isoBoxer_.addBoxProcessor('saiz', function() {
+ // eslint-disable-next-line no-invalid-this
+ const box = /** @type {!ISOBox} */(this);
+ box._procFullBox();
+ if (box.flags & 1) {
+ box._procField('aux_info_type', 'uint', 32);
+ box._procField('aux_info_type_parameter', 'uint', 32);
+ }
+ box._procField('default_sample_info_size', 'uint', 8);
+ box._procField('sample_count', 'uint', 32);
+ if (box.default_sample_info_size === 0) {
+ box._procFieldArray('sample_info_size',
+ box.sample_count, 'uint', 8);
+ }
+ });
+ // eslint-disable-next-line no-restricted-syntax
+ this.isoBoxer_.addBoxProcessor('senc', function() {
+ // eslint-disable-next-line no-invalid-this
+ const box = /** @type {!ISOBox} */(this);
+ box._procFullBox();
+ box._procField('sample_count', 'uint', 32);
+ if (box.flags & 1) {
+ box._procField('IV_size', 'uint', 8);
+ }
+ // eslint-disable-next-line no-restricted-syntax
+ box._procEntries('entry', box.sample_count, function(entry) {
+ // eslint-disable-next-line no-invalid-this
+ const boxEntry = /** @type {!ISOBox} */(this);
+ boxEntry._procEntryField(entry, 'InitializationVector', 'data', 8);
+ if (boxEntry.flags & 2) {
+ boxEntry._procEntryField(entry, 'NumberOfEntries', 'uint', 16);
+ boxEntry._procSubEntries(entry, 'clearAndCryptedData',
+ // eslint-disable-next-line no-restricted-syntax
+ entry.NumberOfEntries, function(clearAndCryptedData) {
+ // eslint-disable-next-line no-invalid-this
+ const subBoxEntry = /** @type {!ISOBox} */(this);
+ subBoxEntry._procEntryField(clearAndCryptedData,
+ 'BytesOfClearData', 'uint', 16);
+ subBoxEntry._procEntryField(clearAndCryptedData,
+ 'BytesOfEncryptedData', 'uint', 32);
+ });
+ }
+ });
+ });
+ }
+
+
+ /**
+ * @override
+ * @export
+ */
+ destroy() {
+ // Nothing
+ }
+
+
+ /**
+ * Check if the mime type and the content type is supported.
+ * @param {string} mimeType
+ * @param {string=} contentType
+ * @return {boolean}
+ * @override
+ * @export
+ */
+ isSupported(mimeType, contentType) {
+ const Capabilities = shaka.media.Capabilities;
+
+ const isMss = mimeType.startsWith('mss/');
+
+ if (!this.isoBoxer_ || !isMss) {
+ return false;
+ }
+
+ if (contentType) {
+ return Capabilities.isTypeSupported(
+ this.convertCodecs(contentType, mimeType));
+ }
+
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
+
+ const audioMime = this.convertCodecs(ContentType.AUDIO, mimeType);
+ const videoMime = this.convertCodecs(ContentType.VIDEO, mimeType);
+ return Capabilities.isTypeSupported(audioMime) ||
+ Capabilities.isTypeSupported(videoMime);
+ }
+
+
+ /**
+ * @override
+ * @export
+ */
+ convertCodecs(contentType, mimeType) {
+ return mimeType.replace('mss/', '');
+ }
+
+
+ /**
+ * @override
+ * @export
+ */
+ getOrginalMimeType() {
+ return this.originalMimeType_;
+ }
+
+
+ /**
+ * @override
+ * @export
+ */
+ transmux(data, stream, reference) {
+ if (!reference) {
+ // Init segment doesn't need transmux
+ return Promise.resolve(shaka.util.BufferUtils.toUint8(data));
+ }
+ if (!stream.mssPrivateData) {
+ return Promise.reject(new shaka.util.Error(
+ shaka.util.Error.Severity.CRITICAL,
+ shaka.util.Error.Category.MEDIA,
+ shaka.util.Error.Code.MSS_MISSING_DATA_FOR_TRANSMUXING));
+ }
+ try {
+ const transmuxedData = this.processMediaSegment_(
+ data, stream, reference);
+ return Promise.resolve(transmuxedData);
+ } catch (exception) {
+ if (exception instanceof shaka.util.Error) {
+ return Promise.reject(exception);
+ }
+ return Promise.reject(new shaka.util.Error(
+ shaka.util.Error.Severity.CRITICAL,
+ shaka.util.Error.Category.MEDIA,
+ shaka.util.Error.Code.MSS_TRANSMUXING_FAILED));
+ }
+ }
+
+ /**
+ * Process a media segment from a data and stream.
+ * @param {BufferSource} data
+ * @param {shaka.extern.Stream} stream
+ * @param {shaka.media.SegmentReference} reference
+ * @return {!Uint8Array}
+ * @private
+ */
+ processMediaSegment_(data, stream, reference) {
+ let i;
+ const isoFile = this.isoBoxer_.parseBuffer(data);
+ // Update track_Id in tfhd box
+ const tfhd = isoFile.fetch('tfhd');
+ tfhd.track_ID = stream.id + 1;
+ // Add tfdt box
+ let tfdt = isoFile.fetch('tfdt');
+ const traf = isoFile.fetch('traf');
+ if (tfdt === null) {
+ tfdt = this.isoBoxer_.createFullBox('tfdt', traf, tfhd);
+ tfdt.version = 1;
+ tfdt.flags = 0;
+ const timescale = stream.mssPrivateData.timescale;
+ const startTime = reference.startTime;
+ tfdt.baseMediaDecodeTime = Math.floor(startTime * timescale);
+ }
+ const trun = isoFile.fetch('trun');
+ // Process tfxd boxes
+ // This box provide absolute timestamp but we take the segment start
+ // time for tfdt
+ let tfxd = isoFile.fetch('tfxd');
+ if (tfxd) {
+ tfxd._parent.boxes.splice(tfxd._parent.boxes.indexOf(tfxd), 1);
+ tfxd = null;
+ }
+ let tfrf = isoFile.fetch('tfrf');
+ if (tfrf) {
+ tfrf._parent.boxes.splice(tfrf._parent.boxes.indexOf(tfrf), 1);
+ tfrf = null;
+ }
+
+ // If protected content in PIFF1.1 format
+ // (sepiff box = Sample Encryption PIFF)
+ // => convert sepiff box it into a senc box
+ // => create saio and saiz boxes (if not already present)
+ const sepiff = isoFile.fetch('sepiff');
+ if (sepiff !== null) {
+ sepiff.type = 'senc';
+ sepiff.usertype = undefined;
+
+ let saio = isoFile.fetch('saio');
+ if (saio === null) {
+ // Create Sample Auxiliary Information Offsets Box box (saio)
+ saio = this.isoBoxer_.createFullBox('saio', traf);
+ saio.version = 0;
+ saio.flags = 0;
+ saio.entry_count = 1;
+ saio.offset = [0];
+ const saiz = this.isoBoxer_.createFullBox('saiz', traf);
+ saiz.version = 0;
+ saiz.flags = 0;
+ saiz.sample_count = sepiff.sample_count;
+ saiz.default_sample_info_size = 0;
+ saiz.sample_info_size = [];
+ if (sepiff.flags & 0x02) {
+ // Sub-sample encryption => set sample_info_size for each sample
+ for (i = 0; i < sepiff.sample_count; i += 1) {
+ // 10 = 8 (InitializationVector field size) + 2
+ // (subsample_count field size)
+ // 6 = 2 (BytesOfClearData field size) + 4
+ // (BytesOfEncryptedData field size)
+ saiz.sample_info_size[i] =
+ 10 + (6 * sepiff.entry[i].NumberOfEntries);
+ }
+ } else {
+ // No sub-sample encryption => set default
+ // sample_info_size = InitializationVector field size (8)
+ saiz.default_sample_info_size = 8;
+ }
+ }
+ }
+
+ // set tfhd.base-data-offset-present to false
+ tfhd.flags &= 0xFFFFFE;
+ // set tfhd.default-base-is-moof to true
+ tfhd.flags |= 0x020000;
+ // set trun.data-offset-present to true
+ trun.flags |= 0x000001;
+
+ // Update trun.data_offset field that corresponds to first data byte
+ // (inside mdat box)
+ const moof = isoFile.fetch('moof');
+ const length = moof.getLength();
+ trun.data_offset = length + 8;
+
+ // Update saio box offset field according to new senc box offset
+ const saio = isoFile.fetch('saio');
+ if (saio !== null) {
+ const trafPosInMoof = this.getBoxOffset_(moof, 'traf');
+ const sencPosInTraf = this.getBoxOffset_(traf, 'senc');
+ // Set offset from begin fragment to the first IV field in senc box
+ // 16 = box header (12) + sample_count field size (4)
+ saio.offset[0] = trafPosInMoof + sencPosInTraf + 16;
+ }
+
+ return shaka.util.BufferUtils.toUint8(isoFile.write());
+ }
+
+ /**
+ * This function returns the offset of the 1st byte of a child box within
+ * a container box.
+ *
+ * @param {ISOBox} parent
+ * @param {string} type
+ * @return {number}
+ * @private
+ */
+ getBoxOffset_(parent, type) {
+ let offset = 8;
+ for (let i = 0; i < parent.boxes.length; i++) {
+ if (parent.boxes[i].type === type) {
+ return offset;
+ }
+ offset += parent.boxes[i].size;
+ }
+ return offset;
+ }
+};
+
+shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
+ 'mss/audio/mp4',
+ () => new shaka.transmuxer.MssTransmuxer('mss/audio/mp4'),
+ shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);
+shaka.transmuxer.TransmuxerEngine.registerTransmuxer(
+ 'mss/video/mp4',
+ () => new shaka.transmuxer.MssTransmuxer('mss/video/mp4'),
+ shaka.transmuxer.TransmuxerEngine.PluginPriority.FALLBACK);
diff --git a/lib/util/error.js b/lib/util/error.js
index a544689b59..0dc56976b4 100644
--- a/lib/util/error.js
+++ b/lib/util/error.js
@@ -497,6 +497,22 @@ shaka.util.Error.Code = {
*/
'CONTENT_TRANSFORMATION_FAILED': 3019,
+ /**
+ * Important data is missing to be able to do the transmuxing of MSS.
+ */
+ 'MSS_MISSING_DATA_FOR_TRANSMUXING': 3020,
+
+ /**
+ * MSS transmuing failed for unknown codec.
+ *
error.data[0] is a unknown codec.
+ */
+ 'MSS_TRANSMUXING_CODEC_UNKNOWN': 3021,
+
+ /**
+ * MSS transmuing failed for unknown reason.
+ */
+ 'MSS_TRANSMUXING_FAILED': 3022,
+
/**
* The Player was unable to guess the manifest type based on file extension
@@ -725,6 +741,17 @@ shaka.util.Error.Code = {
*/
'CANNOT_ADD_EXTERNAL_THUMBNAILS_TO_LIVE_STREAM': 4045,
+ /**
+ * The MSS Manifest contained invalid XML markup.
+ *
error.data[0] is the URI associated with the XML.
+ */
+ 'MSS_INVALID_XML': 4046,
+
+ /**
+ * MSS parser encountered a live playlist.
+ */
+ 'MSS_LIVE_CONTENT_NOT_SUPPORTED': 4047,
+
// RETIRED: 'INCONSISTENT_BUFFER_STATE': 5000,
// RETIRED: 'INVALID_SEGMENT_INDEX': 5001,
diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js
index 6602d3bc83..04b7dac54f 100644
--- a/lib/util/player_configuration.js
+++ b/lib/util/player_configuration.js
@@ -152,6 +152,20 @@ shaka.util.PlayerConfiguration = class {
sequenceMode: supportsSequenceMode,
ignoreManifestTimestampsInSegmentsMode: false,
},
+ mss: {
+ manifestPreprocessor: (element) => {
+ return shaka.util.ConfigUtils.referenceParametersAndReturn(
+ [element],
+ element);
+ },
+ sequenceMode: false,
+ keySystemsBySystemId: {
+ '9a04f079-9840-4286-ab92-e65be0885f95':
+ 'com.microsoft.playready',
+ '79f0049a-4098-8642-ab92-e65be0885f95':
+ 'com.microsoft.playready',
+ },
+ },
};
const streaming = {
diff --git a/lib/util/xml_utils.js b/lib/util/xml_utils.js
index 82ea12d27b..543e986eb3 100644
--- a/lib/util/xml_utils.js
+++ b/lib/util/xml_utils.js
@@ -321,6 +321,19 @@ shaka.util.XmlUtils = class {
}
+ /**
+ * Parses a boolean.
+ * @param {string} booleanString The boolean string.
+ * @return {boolean} The boolean
+ */
+ static parseBoolean(booleanString) {
+ if (!booleanString) {
+ return false;
+ }
+ return booleanString.toLowerCase() === 'true';
+ }
+
+
/**
* Evaluate a division expressed as a string.
* @param {string} exprString
@@ -404,15 +417,15 @@ shaka.util.XmlUtils = class {
/**
- * Parse some UTF8 data and return the resulting root element if
- * it was valid XML.
+ * Parse some data (auto-detecting the encoding) and return the resulting
+ * root element if it was valid XML.
* @param {BufferSource} data
* @param {string} expectedRootElemName
* @return {Element}
*/
static parseXml(data, expectedRootElemName) {
try {
- const string = shaka.util.StringUtils.fromUTF8(data);
+ const string = shaka.util.StringUtils.fromBytesAutoDetect(data);
return shaka.util.XmlUtils.parseXmlString(string, expectedRootElemName);
} catch (exception) {
shaka.log.error('parseXmlString threw!', exception);
diff --git a/package-lock.json b/package-lock.json
index bd88c986c6..36c760bd2a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"babel-plugin-istanbul": "^6.1.1",
"cajon": "^0.4.4",
"code-prettify": "^0.1.0",
+ "codem-isoboxer": "^0.3.7",
"color-themes-for-google-code-prettify": "^2.0.4",
"core-js": "^3.21.1",
"dialog-polyfill": "^0.5.6",
@@ -3106,6 +3107,12 @@
"integrity": "sha1-RocMyMGlDQm61TmzOpg9vUqjSx4=",
"dev": true
},
+ "node_modules/codem-isoboxer": {
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/codem-isoboxer/-/codem-isoboxer-0.3.7.tgz",
+ "integrity": "sha512-aJh5CAuJX0TUUu1aLCd2DKmYxlebJfr1f4PJc9BCfXFbFclHsKvqrnqTrRV5hWVWtisllm+Q03tCEeirow8XAg==",
+ "dev": true
+ },
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -10886,6 +10893,12 @@
"integrity": "sha1-RocMyMGlDQm61TmzOpg9vUqjSx4=",
"dev": true
},
+ "codem-isoboxer": {
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/codem-isoboxer/-/codem-isoboxer-0.3.7.tgz",
+ "integrity": "sha512-aJh5CAuJX0TUUu1aLCd2DKmYxlebJfr1f4PJc9BCfXFbFclHsKvqrnqTrRV5hWVWtisllm+Q03tCEeirow8XAg==",
+ "dev": true
+ },
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
diff --git a/package.json b/package.json
index 19a51fc6b6..323e812cdd 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"babel-plugin-istanbul": "^6.1.1",
"cajon": "^0.4.4",
"code-prettify": "^0.1.0",
+ "codem-isoboxer": "^0.3.7",
"color-themes-for-google-code-prettify": "^2.0.4",
"core-js": "^3.21.1",
"dialog-polyfill": "^0.5.6",
diff --git a/shaka-player.uncompiled.js b/shaka-player.uncompiled.js
index 2cbd8398b5..78c00fea64 100644
--- a/shaka-player.uncompiled.js
+++ b/shaka-player.uncompiled.js
@@ -17,6 +17,7 @@ goog.require('shaka.cast.CastProxy');
goog.require('shaka.cast.CastReceiver');
goog.require('shaka.dash.DashParser');
goog.require('shaka.hls.HlsParser');
+goog.require('shaka.mss.MssParser');
goog.require('shaka.log');
goog.require('shaka.media.AdaptationSetCriteria');
goog.require('shaka.media.InitSegmentReference');
@@ -62,6 +63,7 @@ goog.require('shaka.text.TtmlTextParser');
goog.require('shaka.text.VttTextParser');
goog.require('shaka.text.WebVttGenerator');
goog.require('shaka.transmuxer.TransmuxerEngine');
+goog.require('shaka.transmuxer.MssTransmuxer');
goog.require('shaka.transmuxer.MuxjsTransmuxer');
goog.require('shaka.ui.Controls');
goog.require('shaka.ui.PlayButton');
diff --git a/test/cast/cast_utils_unit.js b/test/cast/cast_utils_unit.js
index ff954fc45b..33440cb602 100644
--- a/test/cast/cast_utils_unit.js
+++ b/test/cast/cast_utils_unit.js
@@ -8,6 +8,9 @@ describe('CastUtils', () => {
const CastUtils = shaka.cast.CastUtils;
const FakeEvent = shaka.util.FakeEvent;
+ /** @type {shaka.extern.Stream} */
+ const fakeStream = shaka.test.StreamingEngineUtil.createMockVideoStream(1);
+
it('includes every Player member', () => {
const ignoredMembers = [
'constructor', // JavaScript added field
@@ -218,11 +221,11 @@ describe('CastUtils', () => {
await mediaSourceEngine.init(initObject, false);
const data = await shaka.test.Util.fetch(initSegmentUrl);
await mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, data, null, null,
+ ContentType.VIDEO, data, null, fakeStream,
/* hasClosedCaptions= */ false);
const data2 = await shaka.test.Util.fetch(videoSegmentUrl);
await mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, data2, null, null,
+ ContentType.VIDEO, data2, null, fakeStream,
/* hasClosedCaptions= */ false);
});
diff --git a/test/demo/demo_unit.js b/test/demo/demo_unit.js
index 6662816eee..3626b1f7f2 100644
--- a/test/demo/demo_unit.js
+++ b/test/demo/demo_unit.js
@@ -95,6 +95,7 @@ describe('Demo', () => {
.add('playRangeEnd')
.add('manifest.dash.keySystemsByURI')
.add('manifest.hls.mediaPlaylistFullMimeType')
+ .add('manifest.mss.keySystemsBySystemId')
.add('drm.keySystemsMapping')
.add('streaming.parsePrftBox');
diff --git a/test/media/drm_engine_integration.js b/test/media/drm_engine_integration.js
index 51caa61975..63851944d6 100644
--- a/test/media/drm_engine_integration.js
+++ b/test/media/drm_engine_integration.js
@@ -49,6 +49,9 @@ describe('DrmEngine', () => {
/** @type {!ArrayBuffer} */
let audioSegment;
+ /** @type {shaka.extern.Stream} */
+ const fakeStream = shaka.test.StreamingEngineUtil.createMockVideoStream(1);
+
beforeAll(async () => {
video = shaka.test.UiUtils.createVideoElement();
document.body.appendChild(video);
@@ -211,10 +214,10 @@ describe('DrmEngine', () => {
await drmEngine.initForPlayback(variants, manifest.offlineSessionIds);
await drmEngine.attach(video);
await mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, videoInitSegment, null,
+ ContentType.VIDEO, videoInitSegment, null, fakeStream,
/* hasClosedCaptions= */ false);
await mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, audioInitSegment, null,
+ ContentType.AUDIO, audioInitSegment, null, fakeStream,
/* hasClosedCaptions= */ false);
await encryptedEventSeen;
// With PlayReady, a persistent license policy can cause a different
@@ -250,10 +253,10 @@ describe('DrmEngine', () => {
const reference = dummyReference(0, 10);
await mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, videoSegment, reference,
+ ContentType.VIDEO, videoSegment, reference, fakeStream,
/* hasClosedCaptions= */ false);
await mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, audioSegment, reference,
+ ContentType.AUDIO, audioSegment, reference, fakeStream,
/* hasClosedCaptions= */ false);
expect(video.buffered.end(0)).toBeGreaterThan(0);
@@ -309,10 +312,10 @@ describe('DrmEngine', () => {
await drmEngine.initForPlayback(variants, manifest.offlineSessionIds);
await drmEngine.attach(video);
await mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, videoInitSegment, null,
+ ContentType.VIDEO, videoInitSegment, null, fakeStream,
/* hasClosedCaptions= */ false);
await mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, audioInitSegment, null,
+ ContentType.AUDIO, audioInitSegment, null, fakeStream,
/* hasClosedCaptions= */ false);
await encryptedEventSeen;
@@ -333,10 +336,10 @@ describe('DrmEngine', () => {
const reference = dummyReference(0, 10);
await mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, videoSegment, reference,
+ ContentType.VIDEO, videoSegment, reference, fakeStream,
/* hasClosedCaptions= */ false);
await mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, audioSegment, reference,
+ ContentType.AUDIO, audioSegment, reference, fakeStream,
/* hasClosedCaptions= */ false);
expect(video.buffered.end(0)).toBeGreaterThan(0);
diff --git a/test/media/media_source_engine_integration.js b/test/media/media_source_engine_integration.js
index 663646dafb..c6788eb568 100644
--- a/test/media/media_source_engine_integration.js
+++ b/test/media/media_source_engine_integration.js
@@ -18,6 +18,9 @@ describe('MediaSourceEngine', () => {
let mediaSourceEngine;
let generators;
let metadata;
+
+ /** @type {shaka.extern.Stream} */
+ const fakeStream = shaka.test.StreamingEngineUtil.createMockVideoStream(1);
// TODO: add text streams to MSE integration tests
const mp4CeaCue0 = jasmine.objectContaining({
@@ -186,7 +189,7 @@ describe('MediaSourceEngine', () => {
const segment = generators[type].getInitSegment(Date.now() / 1000);
const reference = null;
return mediaSourceEngine.appendBuffer(
- type, segment, reference, /* hasClosedCaptions= */ false);
+ type, segment, reference, fakeStream, /* hasClosedCaptions= */ false);
}
function append(type, segmentNumber) {
@@ -194,7 +197,7 @@ describe('MediaSourceEngine', () => {
.getSegment(segmentNumber, Date.now() / 1000);
const reference = dummyReference(type, segmentNumber);
return mediaSourceEngine.appendBuffer(
- type, segment, reference, /* hasClosedCaptions= */ false);
+ type, segment, reference, fakeStream, /* hasClosedCaptions= */ false);
}
function appendWithSeekAndClosedCaptions(type, segmentNumber) {
@@ -205,6 +208,7 @@ describe('MediaSourceEngine', () => {
type,
segment,
reference,
+ fakeStream,
/* hasClosedCaptions= */ true,
/* seeked= */ true);
}
@@ -213,7 +217,7 @@ describe('MediaSourceEngine', () => {
const segment = generators[type].getInitSegment(Date.now() / 1000);
const reference = null;
return mediaSourceEngine.appendBuffer(
- type, segment, reference, /* hasClosedCaptions= */ true);
+ type, segment, reference, fakeStream, /* hasClosedCaptions= */ true);
}
function appendWithClosedCaptions(type, segmentNumber) {
@@ -221,7 +225,7 @@ describe('MediaSourceEngine', () => {
.getSegment(segmentNumber, Date.now() / 1000);
const reference = dummyReference(type, segmentNumber);
return mediaSourceEngine.appendBuffer(
- type, segment, reference, /* hasClosedCaptions= */ true);
+ type, segment, reference, fakeStream, /* hasClosedCaptions= */ true);
}
function buffered(type, time) {
@@ -578,15 +582,16 @@ describe('MediaSourceEngine', () => {
segment, /* offset= */ 0, /* length= */ partialSegmentLength);
let reference = dummyReference(videoType, 0);
await mediaSourceEngine.appendBuffer(
- videoType, partialSegment, reference, /* hasClosedCaptions= */ false);
+ videoType, partialSegment, reference, fakeStream,
+ /* hasClosedCaptions= */ false);
partialSegment = shaka.util.BufferUtils.toUint8(
segment,
/* offset= */ partialSegmentLength);
reference = dummyReference(videoType, 1);
await mediaSourceEngine.appendBuffer(
- videoType, partialSegment, reference, /* hasClosedCaptions= */ false,
- /* seeked= */ true);
+ videoType, partialSegment, reference, fakeStream,
+ /* hasClosedCaptions= */ false, /* seeked= */ true);
});
it('extracts CEA-708 captions from dash', async () => {
diff --git a/test/media/media_source_engine_unit.js b/test/media/media_source_engine_unit.js
index 4806977212..cc99561dff 100644
--- a/test/media/media_source_engine_unit.js
+++ b/test/media/media_source_engine_unit.js
@@ -61,6 +61,9 @@ describe('MediaSourceEngine', () => {
const fakeTextStream = {mimeType: 'text/foo', drmInfos: []};
const fakeTransportStream = {mimeType: 'tsMimetype', drmInfos: []};
+ /** @type {shaka.extern.Stream} */
+ const fakeStream = shaka.test.StreamingEngineUtil.createMockVideoStream(1);
+
let audioSourceBuffer;
let videoSourceBuffer;
let mockVideo;
@@ -365,7 +368,7 @@ describe('MediaSourceEngine', () => {
it('appends the given data', async () => {
const p = mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false);
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
audioSourceBuffer.updateend();
@@ -383,7 +386,7 @@ describe('MediaSourceEngine', () => {
{code: 5, message: 'something failed'}));
await expectAsync(
mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false))
.toBeRejectedWith(expected);
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
@@ -402,7 +405,7 @@ describe('MediaSourceEngine', () => {
ContentType.AUDIO));
await expectAsync(
mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false))
.toBeRejectedWith(expected);
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
@@ -423,10 +426,10 @@ describe('MediaSourceEngine', () => {
ContentType.AUDIO));
const p1 = mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false);
const p2 = mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false);
audioSourceBuffer.updateend();
await expectAsync(p1).toBeResolved();
@@ -436,7 +439,7 @@ describe('MediaSourceEngine', () => {
it('rejects the promise if this operation fails async', async () => {
mockVideo.error = {code: 5};
const p = mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false);
audioSourceBuffer.error();
audioSourceBuffer.updateend();
@@ -453,11 +456,11 @@ describe('MediaSourceEngine', () => {
it('queues operations on a single SourceBuffer', async () => {
/** @type {!shaka.test.StatusPromise} */
const p1 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false));
/** @type {!shaka.test.StatusPromise} */
const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer2, null,
+ ContentType.AUDIO, buffer2, null, fakeStream,
/* hasClosedCaptions= */ false));
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
@@ -476,15 +479,15 @@ describe('MediaSourceEngine', () => {
it('queues operations independently for different types', async () => {
/** @type {!shaka.test.StatusPromise} */
const p1 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false));
/** @type {!shaka.test.StatusPromise} */
const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer2, null,
+ ContentType.AUDIO, buffer2, null, fakeStream,
/* hasClosedCaptions= */ false));
/** @type {!shaka.test.StatusPromise} */
const p3 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, buffer3, null,
+ ContentType.VIDEO, buffer3, null, fakeStream,
/* hasClosedCaptions= */ false));
expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer);
@@ -519,13 +522,13 @@ describe('MediaSourceEngine', () => {
});
const p1 = mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false);
const p2 = mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer2, null,
+ ContentType.AUDIO, buffer2, null, fakeStream,
/* hasClosedCaptions= */ false);
const p3 = mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer3, null,
+ ContentType.AUDIO, buffer3, null, fakeStream,
/* hasClosedCaptions= */ false);
await expectAsync(p1).toBeResolved();
@@ -541,7 +544,8 @@ describe('MediaSourceEngine', () => {
expect(mockTextEngine.appendBuffer).not.toHaveBeenCalled();
const reference = dummyReference(0, 10);
await mediaSourceEngine.appendBuffer(
- ContentType.TEXT, data, reference, /* hasClosedCaptions= */ false);
+ ContentType.TEXT, data, reference, fakeStream,
+ /* hasClosedCaptions= */ false);
expect(mockTextEngine.appendBuffer).toHaveBeenCalledWith(
data, 0, 10);
});
@@ -559,7 +563,7 @@ describe('MediaSourceEngine', () => {
const init = async () => {
await mediaSourceEngine.init(initObject, false);
await mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, buffer, null,
+ ContentType.VIDEO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false);
expect(videoSourceBuffer.appendBuffer).toHaveBeenCalled();
};
@@ -587,7 +591,7 @@ describe('MediaSourceEngine', () => {
// Initialize the closed caption parser.
const appendInit = mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, buffer, null,
+ ContentType.VIDEO, buffer, null, fakeStream,
/* hasClosedCaptions= */ true);
// In MediaSourceEngine, appendBuffer() is async and Promise-based, but
// at the browser level, it's event-based.
@@ -603,7 +607,8 @@ describe('MediaSourceEngine', () => {
// Parse and append the closed captions embedded in video stream.
const reference = dummyReference(0, 1000);
const appendVideo = mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, buffer, reference, true);
+ ContentType.VIDEO, buffer, reference, fakeStream,
+ /* hasClosedCaptions= */ true);
videoSourceBuffer.updateend();
await appendVideo;
@@ -624,7 +629,8 @@ describe('MediaSourceEngine', () => {
const reference = dummyReference(0, 1000);
reference.startTime = 0.50;
const appendVideo = mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, buffer, reference, /* hasClosedCaptions= */ false,
+ ContentType.VIDEO, buffer, reference, fakeStream,
+ /* hasClosedCaptions= */ false,
/* seeked= */ false, /* adaptation= */ true);
videoSourceBuffer.updateend();
await appendVideo;
@@ -646,7 +652,8 @@ describe('MediaSourceEngine', () => {
// text segments. In this case, SourceBuffer mode is still 'segments'.
let reference = dummyReference(0, 1000);
let appendVideo = mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, buffer, reference, /* hasClosedCaptions= */ false);
+ ContentType.VIDEO, buffer, reference, fakeStream,
+ /* hasClosedCaptions= */ false);
// Wait for the first appendBuffer(), in segments mode.
await simulateUpdate();
// Next, wait for abort(), used to reset the parser state for a safe
@@ -668,8 +675,8 @@ describe('MediaSourceEngine', () => {
// unbuffered seek or adaptation. SourceBuffer mode is 'sequence' now.
reference = dummyReference(0, 1000);
appendVideo = mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, buffer, reference, /* hasClosedCaptions= */ false,
- /* seeked= */ true);
+ ContentType.VIDEO, buffer, reference, fakeStream,
+ /* hasClosedCaptions= */ false, /* seeked= */ true);
// First, wait for abort(), used to reset the parser state for a safe
// setting of timestampOffset.
await Util.shortDelay();
@@ -903,11 +910,11 @@ describe('MediaSourceEngine', () => {
it('waits for all previous operations to complete', async () => {
/** @type {!shaka.test.StatusPromise} */
const p1 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false));
/** @type {!shaka.test.StatusPromise} */
const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, buffer, null,
+ ContentType.VIDEO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false));
/** @type {!shaka.test.StatusPromise} */
const p3 = new shaka.test.StatusPromise(mediaSourceEngine.endOfStream());
@@ -931,11 +938,11 @@ describe('MediaSourceEngine', () => {
/** @type {!Promise} */
const p1 = mediaSourceEngine.endOfStream();
mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null,
- /* hasClosedCaptions= */ false);
+ fakeStream, /* hasClosedCaptions= */ false);
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null,
- /* hasClosedCaptions= */ false);
+ fakeStream, /* hasClosedCaptions= */ false);
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer2, null,
- /* hasClosedCaptions= */ false);
+ fakeStream, /* hasClosedCaptions= */ false);
// endOfStream hasn't been called yet because blocking multiple queues
// takes an extra tick, even when they are empty.
@@ -963,7 +970,7 @@ describe('MediaSourceEngine', () => {
/** @type {!Promise} */
const p1 = mediaSourceEngine.endOfStream();
mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null,
- /* hasClosedCaptions= */ false);
+ fakeStream, /* hasClosedCaptions= */ false);
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalled();
@@ -994,11 +1001,11 @@ describe('MediaSourceEngine', () => {
it('waits for all previous operations to complete', async () => {
/** @type {!shaka.test.StatusPromise} */
const p1 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false));
/** @type {!shaka.test.StatusPromise} */
const p2 = new shaka.test.StatusPromise(mediaSourceEngine.appendBuffer(
- ContentType.VIDEO, buffer, null,
+ ContentType.VIDEO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false));
/** @type {!shaka.test.StatusPromise} */
const p3 =
@@ -1023,11 +1030,11 @@ describe('MediaSourceEngine', () => {
/** @type {!Promise} */
const p1 = mediaSourceEngine.setDuration(100);
mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null,
- /* hasClosedCaptions= */ false);
+ fakeStream, /* hasClosedCaptions= */ false);
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null,
- /* hasClosedCaptions= */ false);
+ fakeStream, /* hasClosedCaptions= */ false);
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer2, null,
- /* hasClosedCaptions= */ false);
+ fakeStream, /* hasClosedCaptions= */ false);
// The setter hasn't been called yet because blocking multiple queues
// takes an extra tick, even when they are empty.
@@ -1056,7 +1063,7 @@ describe('MediaSourceEngine', () => {
/** @type {!Promise} */
const p1 = mediaSourceEngine.setDuration(100);
mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null,
- /* hasClosedCaptions= */ false);
+ fakeStream, /* hasClosedCaptions= */ false);
expect(audioSourceBuffer.appendBuffer).not.toHaveBeenCalled();
@@ -1095,9 +1102,9 @@ describe('MediaSourceEngine', () => {
// This is tested because shrinking duration generates 'updateend'
// events, and we want to show that the queue still operates correctly.
const a1 = mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null,
- /* hasClosedCaptions= */ false);
+ fakeStream, /* hasClosedCaptions= */ false);
const a2 = mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null,
- /* hasClosedCaptions= */ false);
+ fakeStream, /* hasClosedCaptions= */ false);
await p1;
await a1;
@@ -1117,9 +1124,9 @@ describe('MediaSourceEngine', () => {
it('waits for all operations to complete', async () => {
mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null,
- /* hasClosedCaptions= */ false);
+ fakeStream, /* hasClosedCaptions= */ false);
mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null,
- /* hasClosedCaptions= */ false);
+ fakeStream, /* hasClosedCaptions= */ false);
/** @type {!shaka.test.StatusPromise} */
const d = new shaka.test.StatusPromise(mediaSourceEngine.destroy());
@@ -1136,7 +1143,7 @@ describe('MediaSourceEngine', () => {
it('resolves even when a pending operation fails', async () => {
const p = mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false);
const d = mediaSourceEngine.destroy();
@@ -1161,10 +1168,10 @@ describe('MediaSourceEngine', () => {
it('cancels operations that have not yet started', async () => {
mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false);
const rejected = mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer2, null,
+ ContentType.AUDIO, buffer2, null, fakeStream,
/* hasClosedCaptions= */ false);
// Create the expectation first so we don't get unhandled rejection errors
const expected = expectAsync(rejected).toBeRejected();
@@ -1188,7 +1195,7 @@ describe('MediaSourceEngine', () => {
it('cancels blocking operations that have not yet started', async () => {
const p1 = mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false);
const p2 = mediaSourceEngine.endOfStream();
const d = mediaSourceEngine.destroy();
@@ -1203,7 +1210,7 @@ describe('MediaSourceEngine', () => {
const d = mediaSourceEngine.destroy();
await expectAsync(
mediaSourceEngine.appendBuffer(
- ContentType.AUDIO, buffer, null,
+ ContentType.AUDIO, buffer, null, fakeStream,
/* hasClosedCaptions= */ false))
.toBeRejected();
await d;
diff --git a/test/mss/mss_parser_content_protection_unit.js b/test/mss/mss_parser_content_protection_unit.js
new file mode 100644
index 0000000000..cc160c0f10
--- /dev/null
+++ b/test/mss/mss_parser_content_protection_unit.js
@@ -0,0 +1,49 @@
+/*! @license
+ * Shaka Player
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// Test DRM-related parsing.
+describe('MssParser ContentProtection', () => {
+ const ContentProtection = shaka.mss.ContentProtection;
+
+ const strToXml = (str) => {
+ const parser = new DOMParser();
+ return parser.parseFromString(str, 'application/xml').documentElement;
+ };
+
+ it('getPlayReadyLicenseURL', () => {
+ const laurl = [
+ '',
+ ' ',
+ ' www.example.com',
+ ' ',
+ '',
+ ].join('\n');
+ const laurlCodes = laurl.split('').map((c) => {
+ return c.charCodeAt();
+ });
+ const prBytes = new Uint16Array([
+ // pr object size (in num bytes).
+ // + 10 for PRO size, count, and type
+ laurl.length * 2 + 10, 0,
+ // record count
+ 1,
+ // type
+ ContentProtection.PLAYREADY_RECORD_TYPES.RIGHTS_MANAGEMENT,
+ // record size (in num bytes)
+ laurl.length * 2,
+ // value
+ ].concat(laurlCodes));
+
+ const encodedPrObject = shaka.util.Uint8ArrayUtils.toBase64(prBytes);
+ const input = strToXml([
+ '',
+ encodedPrObject,
+ '',
+ ].join('\n'));
+ const actual = ContentProtection.getPlayReadyLicenseUrl(input);
+ expect(actual).toBe('www.example.com');
+ });
+});
diff --git a/test/mss/mss_parser_unit.js b/test/mss/mss_parser_unit.js
new file mode 100644
index 0000000000..23d8b1e5ad
--- /dev/null
+++ b/test/mss/mss_parser_unit.js
@@ -0,0 +1,420 @@
+/*! @license
+ * Shaka Player
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// Test basic manifest parsing functionality.
+describe('MssParser Manifest', () => {
+ // const ManifestParser = shaka.test.ManifestParser;
+ const Mss = shaka.test.Mss;
+
+ /** @type {!shaka.test.FakeNetworkingEngine} */
+ let fakeNetEngine;
+ /** @type {!shaka.mss.MssParser} */
+ let parser;
+ /** @type {!jasmine.Spy} */
+ let onEventSpy;
+ /** @type {shaka.extern.ManifestParser.PlayerInterface} */
+ let playerInterface;
+
+ const h264CodecPrivateData = '000000016764001FAC2CA5014016EFFC100010014808' +
+ '080A000007D200017700C100005A648000B4C9FE31C6080002D3240005A64FF18E1DA' +
+ '12251600000000168E9093525';
+
+ const aacCodecPrivateData = '1210';
+
+ /** @param {!shaka.extern.Manifest} manifest */
+ async function loadAllStreamsFor(manifest) {
+ const promises = [];
+ for (const variant of manifest.variants) {
+ for (const stream of [variant.video, variant.audio]) {
+ if (stream) {
+ promises.push(stream.createSegmentIndex());
+ }
+ }
+ }
+ for (const text of manifest.textStreams) {
+ promises.push(text.createSegmentIndex());
+ }
+ await Promise.all(promises);
+ }
+
+ beforeEach(() => {
+ fakeNetEngine = new shaka.test.FakeNetworkingEngine();
+ parser = Mss.makeMssParser();
+ onEventSpy = jasmine.createSpy('onEvent');
+ playerInterface = {
+ networkingEngine: fakeNetEngine,
+ modifyManifestRequest: (request, manifestInfo) => {},
+ modifySegmentRequest: (request, segmentInfo) => {},
+ filter: (manifest) => Promise.resolve(),
+ makeTextStreamsForClosedCaptions: (manifest) => {},
+ onTimelineRegionAdded: fail, // Should not have any EventStream elements.
+ onEvent: shaka.test.Util.spyFunc(onEventSpy),
+ onError: fail,
+ isLowLatencyMode: () => false,
+ isAutoLowLatencyMode: () => false,
+ enableLowLatencyMode: () => {},
+ updateDuration: () => {},
+ newDrmInfo: (stream) => {},
+ };
+ });
+
+ describe('fails for', () => {
+ it('invalid XML', async () => {
+ const source = ' {
+ const source = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ].join('\n');
+ const error = new shaka.util.Error(
+ shaka.util.Error.Severity.CRITICAL,
+ shaka.util.Error.Category.MANIFEST,
+ shaka.util.Error.Code.MSS_INVALID_XML,
+ 'dummy://foo');
+ await Mss.testFails(source, error);
+ });
+
+ it('failed network requests', async () => {
+ const expectedError = new shaka.util.Error(
+ shaka.util.Error.Severity.CRITICAL,
+ shaka.util.Error.Category.NETWORK,
+ shaka.util.Error.Code.BAD_HTTP_STATUS);
+
+ fakeNetEngine.request.and.returnValue(
+ shaka.util.AbortableOperation.failed(expectedError));
+ await expectAsync(parser.start('', playerInterface))
+ .toBeRejectedWith(shaka.test.Util.jasmineError(expectedError));
+ });
+
+ it('missing SmoothStreamingMedia element', async () => {
+ const source = '';
+ const error = new shaka.util.Error(
+ shaka.util.Error.Severity.CRITICAL,
+ shaka.util.Error.Category.MANIFEST,
+ shaka.util.Error.Code.MSS_INVALID_XML,
+ 'dummy://foo');
+ await Mss.testFails(source, error);
+ });
+
+ it('ive content ', async () => {
+ const source = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ ].join('\n');
+ const error = new shaka.util.Error(
+ shaka.util.Error.Severity.CRITICAL,
+ shaka.util.Error.Category.MANIFEST,
+ shaka.util.Error.Code.MSS_LIVE_CONTENT_NOT_SUPPORTED);
+ await Mss.testFails(source, error);
+ });
+ });
+
+ it('Disable audio does not create audio streams', async () => {
+ const manifestText = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ ].join('\n');
+
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
+ const config = shaka.util.PlayerConfiguration.createDefault().manifest;
+ config.disableAudio = true;
+ parser.configure(config);
+
+ /** @type {shaka.extern.Manifest} */
+ const manifest = await parser.start('dummy://foo', playerInterface);
+ const variant = manifest.variants[0];
+ expect(variant.audio).toBe(null);
+ expect(variant.video).toBeTruthy();
+ });
+
+ it('Disable video does not create video streams', async () => {
+ const manifestText = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ ].join('\n');
+
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
+ const config = shaka.util.PlayerConfiguration.createDefault().manifest;
+ config.disableVideo = true;
+ parser.configure(config);
+
+ /** @type {shaka.extern.Manifest} */
+ const manifest = await parser.start('dummy://foo', playerInterface);
+ const variant = manifest.variants[0];
+ expect(variant.audio).toBeTruthy();
+ expect(variant.video).toBe(null);
+ });
+
+ it('Disable text does not create text streams', async () => {
+ const manifestText = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ ].join('\n');
+
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
+ const config = shaka.util.PlayerConfiguration.createDefault().manifest;
+ config.disableText = true;
+ parser.configure(config);
+
+ /** @type {shaka.extern.Manifest} */
+ const manifest = await parser.start('dummy://foo', playerInterface);
+ const stream = manifest.textStreams[0];
+ expect(stream).toBeUndefined();
+ });
+
+ it('Invokes manifestPreprocessor in config', async () => {
+ const manifestText = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ ].join('\n');
+
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
+ const config = shaka.util.PlayerConfiguration.createDefault().manifest;
+ config.mss.manifestPreprocessor = (mss) => {
+ const selector = 'StreamIndex[Name="text"';
+ const vttElements = mss.querySelectorAll(selector);
+ for (const element of vttElements) {
+ element.parentNode.removeChild(element);
+ }
+ };
+ parser.configure(config);
+
+ /** @type {shaka.extern.Manifest} */
+ const manifest = await parser.start('dummy://foo', playerInterface);
+ const stream = manifest.textStreams[0];
+ expect(stream).toBeUndefined();
+ });
+
+ it('generate a fake init segment', async () => {
+ const manifestText = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ ].join('\n');
+
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
+
+ const manifest = await parser.start('dummy://foo', playerInterface);
+ const segmentReference =
+ await Mss.getFirstAudioSegmentReference(manifest);
+ const initSegmentReference = segmentReference.initSegmentReference;
+ expect(initSegmentReference.getUris()).toEqual([]);
+ expect(initSegmentReference.getStartByte()).toBe(0);
+ expect(initSegmentReference.getEndByte()).toBe(null);
+ expect(initSegmentReference.getSegmentData()).toBeDefined();
+ });
+
+ it('skip video stream without CodecPrivateData', async () => {
+ const manifestText = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ ].join('\n');
+
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
+
+ /** @type {shaka.extern.Manifest} */
+ const manifest = await parser.start('dummy://foo', playerInterface);
+ const variant = manifest.variants[0];
+ expect(variant.audio).toBeTruthy();
+ expect(variant.video).toBe(null);
+ });
+
+ it('skip video stream without FourCC', async () => {
+ const manifestText = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ ].join('\n');
+
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
+
+ /** @type {shaka.extern.Manifest} */
+ const manifest = await parser.start('dummy://foo', playerInterface);
+ const variant = manifest.variants[0];
+ expect(variant.audio).toBeTruthy();
+ expect(variant.video).toBe(null);
+ });
+
+ it('supports audio stream without FourCC', async () => {
+ const manifestText = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ ].join('\n');
+
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
+
+ /** @type {shaka.extern.Manifest} */
+ const manifest = await parser.start('dummy://foo', playerInterface);
+ const variant = manifest.variants[0];
+ expect(variant.audio).toBeTruthy();
+ expect(variant.video).toBeTruthy();
+ });
+
+ it('supports AACL stream without CodecPrivateData', async () => {
+ const manifestText = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ ].join('\n');
+
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
+
+ /** @type {shaka.extern.Manifest} */
+ const manifest = await parser.start('dummy://foo', playerInterface);
+ const variant = manifest.variants[0];
+ expect(variant.audio).toBeTruthy();
+ expect(variant.video).toBeTruthy();
+ });
+
+ it('supports AACH stream without CodecPrivateData', async () => {
+ const manifestText = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ ].join('\n');
+
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
+
+ /** @type {shaka.extern.Manifest} */
+ const manifest = await parser.start('dummy://foo', playerInterface);
+ const variant = manifest.variants[0];
+ expect(variant.audio).toBeTruthy();
+ expect(variant.video).toBeTruthy();
+ });
+});
diff --git a/test/mss/mss_player_integration.js b/test/mss/mss_player_integration.js
new file mode 100644
index 0000000000..e136df95ef
--- /dev/null
+++ b/test/mss/mss_player_integration.js
@@ -0,0 +1,84 @@
+/*! @license
+ * Shaka Player
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+describe('MSS Player', () => {
+ const Util = shaka.test.Util;
+
+ /** @type {!jasmine.Spy} */
+ let onErrorSpy;
+
+ /** @type {!HTMLVideoElement} */
+ let video;
+ /** @type {shaka.Player} */
+ let player;
+ /** @type {!shaka.util.EventManager} */
+ let eventManager;
+
+ let compiledShaka;
+
+ /** @type {!shaka.test.Waiter} */
+ let waiter;
+
+ // eslint-disable-next-line max-len
+ const url = 'https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest';
+
+ beforeAll(async () => {
+ video = shaka.test.UiUtils.createVideoElement();
+ document.body.appendChild(video);
+ compiledShaka =
+ await shaka.test.Loader.loadShaka(getClientArg('uncompiled'));
+ });
+
+ beforeEach(() => {
+ player = new compiledShaka.Player(video);
+
+ // Grab event manager from the uncompiled library:
+ eventManager = new shaka.util.EventManager();
+ waiter = new shaka.test.Waiter(eventManager);
+ waiter.setPlayer(player);
+
+ onErrorSpy = jasmine.createSpy('onError');
+ onErrorSpy.and.callFake((event) => fail(event.detail));
+ eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
+ });
+
+ afterEach(async () => {
+ eventManager.release();
+ await player.destroy();
+ });
+
+ afterAll(() => {
+ document.body.removeChild(video);
+ });
+
+ it('MSS VoD', async () => {
+ // Make sure we are playing the lowest res available to avoid test flake
+ // based on network issues. Note that disabling ABR and setting a low
+ // abr.defaultBandwidthEstimate would not be sufficient, because it
+ // would only affect the choice of track on the first period. When we
+ // cross a period boundary, the default bandwidth estimate will no
+ // longer be in effect, and AbrManager may choose higher res tracks for
+ // the new period. Using abr.restrictions.maxHeight will let us force
+ // AbrManager to the lowest resolution, which is its fallback when these
+ // soft restrictions cannot be met.
+ player.configure('abr.restrictions.maxHeight', 1);
+
+ await player.load(url, /* startTime= */ null,
+ /* mimeType= */ 'application/vnd.ms-sstr+xml');
+ video.play();
+ expect(player.isLive()).toBe(false);
+
+ // Wait for the video to start playback. If it takes longer than 10
+ // seconds, fail the test.
+ await waiter.waitForMovementOrFailOnTimeout(video, 10);
+
+ // Play for 5 seconds, but stop early if the video ends. If it takes
+ // longer than 10 seconds, fail the test.
+ await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 5, 10);
+
+ await player.unload();
+ });
+});
diff --git a/test/test/util/mss_parser_util.js b/test/test/util/mss_parser_util.js
new file mode 100644
index 0000000000..ca5fd14e38
--- /dev/null
+++ b/test/test/util/mss_parser_util.js
@@ -0,0 +1,115 @@
+/*! @license
+ * Shaka Player
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/** @summary Utilities for working with the MSS parser. */
+shaka.test.Mss = class {
+ /**
+ * Constructs and configures a very simple MSS parser.
+ * @return {!shaka.mss.MssParser}
+ */
+ static makeMssParser() {
+ const parser = new shaka.mss.MssParser();
+ const config = shaka.util.PlayerConfiguration.createDefault().manifest;
+ parser.configure(config);
+ return parser;
+ }
+
+ /**
+ * Tests the segment index produced by the MSS manifest parser.
+ *
+ * @param {string} manifestText
+ * @param {!Array.} references
+ * @return {!Promise}
+ */
+ static async testSegmentIndex(manifestText, references) {
+ const buffer = shaka.util.StringUtils.toUTF8(manifestText);
+ const mssParser = shaka.test.Mss.makeMssParser();
+
+ const networkingEngine = new shaka.test.FakeNetworkingEngine()
+ .setResponseValue('dummy://foo', buffer);
+
+ const playerInterface = {
+ networkingEngine: networkingEngine,
+ modifyManifestRequest: (request, manifestInfo) => {},
+ modifySegmentRequest: (request, segmentInfo) => {},
+ filter: () => {},
+ makeTextStreamsForClosedCaptions: (manifest) => {},
+ onTimelineRegionAdded: fail,
+ onEvent: fail,
+ onError: fail,
+ isLowLatencyMode: () => false,
+ isAutoLowLatencyMode: () => false,
+ enableLowLatencyMode: () => {},
+ updateDuration: () => {},
+ newDrmInfo: (stream) => {},
+ };
+ const manifest = await mssParser.start('dummy://foo', playerInterface);
+ const stream = manifest.variants[0].audio;
+ await stream.createSegmentIndex();
+
+ shaka.test.ManifestParser.verifySegmentIndex(stream, references);
+ }
+
+ /**
+ * Tests that the MSS manifest parser fails to parse the given manifest.
+ *
+ * @param {string} manifestText
+ * @param {!shaka.util.Error} expectedError
+ * @return {!Promise}
+ */
+ static async testFails(manifestText, expectedError) {
+ const manifestData = shaka.util.StringUtils.toUTF8(manifestText);
+ const mssParser = shaka.test.Mss.makeMssParser();
+
+ const networkingEngine = new shaka.test.FakeNetworkingEngine()
+ .setResponseValue('dummy://foo', manifestData);
+
+ const playerInterface = {
+ networkingEngine: networkingEngine,
+ modifyManifestRequest: (request, manifestInfo) => {},
+ modifySegmentRequest: (request, segmentInfo) => {},
+ filter: () => {},
+ makeTextStreamsForClosedCaptions: (manifest) => {},
+ onTimelineRegionAdded: fail, // Should not have any EventStream elements.
+ onEvent: fail,
+ onError: fail,
+ isLowLatencyMode: () => false,
+ isAutoLowLatencyMode: () => false,
+ enableLowLatencyMode: () => {},
+ updateDuration: () => {},
+ newDrmInfo: (stream) => {},
+ };
+ const p = mssParser.start('dummy://foo', playerInterface);
+ await expectAsync(p).toBeRejectedWith(
+ shaka.test.Util.jasmineError(expectedError));
+ }
+
+ /**
+ * @param {shaka.extern.Manifest} manifest
+ * @return {!Promise.}
+ */
+ static async getFirstAudioSegmentReference(manifest) {
+ const variant = manifest.variants[0];
+ expect(variant).not.toBe(null);
+ if (!variant) {
+ return null;
+ }
+
+ const audio = variant.audio;
+ expect(audio).not.toBe(null);
+ if (!audio) {
+ return null;
+ }
+
+ await audio.createSegmentIndex();
+ const position = audio.segmentIndex.find(0);
+ goog.asserts.assert(position != null, 'Position should not be null!');
+
+ const reference = audio.segmentIndex.get(position);
+ goog.asserts.assert(reference != null, 'Reference should not be null!');
+ return reference;
+ }
+};