diff --git a/README.md b/README.md
index fd058950ab1..24652f24142 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
# 
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 it's necessary [codem-isoboxer][] v0.3.7+
- 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/types/manifests b/build/types/manifests
index 29426067ac7..5e5b0450a54 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 00000000000..a9f330d2ec7
--- /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 9b52e56f33f..35537d7372f 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 c938b82673d..6ec01581be4 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',
@@ -241,6 +243,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 b72ab6293c2..999b2f43da4 100644
--- a/demo/config.js
+++ b/demo/config.js
@@ -252,7 +252,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 db4a3a342ba..eb8c41a3890 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 8e529baeab5..1b2d20ff9f0 100644
--- a/demo/locales/en.json
+++ b/demo/locales/en.json
@@ -167,6 +167,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.",
@@ -244,6 +246,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 1069b210a50..dd1e80135f6 100644
--- a/demo/locales/source.json
+++ b/demo/locales/source.json
@@ -675,6 +675,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"
@@ -979,6 +987,10 @@
"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 offline: the browser cannot play MSS (https://en.wikipedia.org/wiki/Adaptive_bitrate_streaming#Microsoft_Smooth_Streaming_(MSS)) content.",
+ "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 e8fb3b87b11..79f69ccff23 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 0d5bcefd185..28215be439a 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 64dd61ba71e..1d2f3b5cc93 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/externs/isoboxer.js b/externs/isoboxer.js
new file mode 100644
index 00000000000..0aafd0e9f6a
--- /dev/null
+++ b/externs/isoboxer.js
@@ -0,0 +1,94 @@
+/*! @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,
+ * _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))
+ * }}
+ * @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_sizes
+ * @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
+ * @const
+ */
+var ISOBox;
+
+
+/**
+ * @typedef {{
+ * NumberOfEntries: number
+ * }}
+ * @property {number} NumberOfEntries
+ * @const
+ */
+var ISOEntry;
diff --git a/externs/shaka/manifest.js b/externs/shaka/manifest.js
index 4f8d30ed50b..390d36b965c 100644
--- a/externs/shaka/manifest.js
+++ b/externs/shaka/manifest.js
@@ -325,7 +325,8 @@ shaka.extern.FetchCryptoKeysFunction;
* tilesLayout: (string|undefined),
* matchedStreams:
* (!Array.|!Array.|
- * undefined)
+ * undefined),
+ * mssPrivateData: (shaka.extern.MssPrivateData|undefined)
* }}
*
* @description
@@ -438,7 +439,35 @@ 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
+ * Required.
+ * MSS codecPrivateData.
+ *
+ * @exportDoc
+ */
+shaka.extern.MssPrivateData;
diff --git a/externs/shaka/player.js b/externs/shaka/player.js
index 5c901660c9c..971ad27f7ee 100644
--- a/externs/shaka/player.js
+++ b/externs/shaka/player.js
@@ -892,6 +892,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,
@@ -903,7 +926,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
@@ -941,6 +965,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 091581754d2..c61c434b8a4 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 ffddd3bb4ae..de08c470fa6 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 faab54c306d..cf12fa8b856 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 94d2f2b8945..7f48da48ad9 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 bab68e038e4..0f65a890c17 100644
--- a/lib/media/media_source_engine.js
+++ b/lib/media/media_source_engine.js
@@ -573,6 +573,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
@@ -581,7 +582,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;
@@ -730,7 +731,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 cfde258c8f1..1100f2543de 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 d454f0f99d6..b93b7ec5248 100644
--- a/lib/media/streaming_engine.js
+++ b/lib/media/streaming_engine.js
@@ -1686,11 +1686,18 @@ shaka.media.StreamingEngine = class {
if (reference.initSegmentReference) {
shaka.log.v1(logPrefix, 'fetching init segment');
- const fetchInit =
- this.fetch_(mediaState, reference.initSegmentReference);
+ let fetchInit;
+ if (!reference.initSegmentReference.getSegmentData()) {
+ fetchInit =
+ this.fetch_(mediaState, reference.initSegmentReference);
+ }
const append = async () => {
try {
- const initSegment = await fetchInit;
+ let initSegment =
+ reference.initSegmentReference.getSegmentData();
+ if (!initSegment) {
+ initSegment = await fetchInit;
+ }
this.destroyer_.ensureNotDestroyed();
const parser = new shaka.util.Mp4Parser();
@@ -1710,7 +1717,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;
@@ -1804,6 +1811,7 @@ shaka.media.StreamingEngine = class {
mediaState.type,
segment,
reference,
+ stream,
hasClosedCaptions,
seeked,
adaptation);
diff --git a/lib/mss/content_protection.js b/lib/mss/content_protection.js
new file mode 100644
index 00000000000..9d2e7203164
--- /dev/null
+++ b/lib/mss/content_protection.js
@@ -0,0 +1,337 @@
+/*! @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);
+ }
+
+ /**
+ * PlayReady Header format: https://goo.gl/dBzxNA
+ *
+ * @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')) {
+ for (const child of elem.childNodes) {
+ if (child instanceof Element && child.tagName == 'LA_URL') {
+ return child.textContent;
+ }
+ }
+ }
+
+ // Not found
+ 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 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 '';
+ }
+
+ const xml = shaka.util.StringUtils.fromUTF16(record.value, true);
+ const rootElement = shaka.util.XmlUtils.parseXmlString(xml, 'WRMHEADER');
+ if (!rootElement) {
+ return '';
+ }
+
+ return ContentProtection.getLaurl_(rootElement);
+ }
+
+ /**
+ * PlayReady Header format: https://goo.gl/dBzxNA
+ *
+ * @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')) {
+ for (const child of elem.childNodes) {
+ if (child instanceof Element && child.tagName == 'KID') {
+ return child.textContent;
+ }
+ }
+ }
+
+ // Not found
+ return null;
+ }
+
+ /**
+ * Gets a PlayReady KID from a protection element
+ * containing a PlayReady Header Object
+ *
+ * @param {!Element} element
+ * @return {?string}
+ */
+ static getPlayReadyKID(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 ContentProtection.getKID_(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 the given element.
+ *
+ * @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 00000000000..9babb80ee1b
--- /dev/null
+++ b/lib/mss/mss_parser.js
@@ -0,0 +1,967 @@
+/*! @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.CmcdManager');
+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_();
+
+ if (this.playerInterface_) {
+ this.setUpdateTimer_();
+ }
+
+ // 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);
+ }
+
+ 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.
+ *
+ * @return {!Promise.} Resolves with the time it took, in seconds, to
+ * fulfill the request and parse the data.
+ * @private
+ */
+ async requestManifest_() {
+ const requestType = shaka.net.NetworkingEngine.RequestType.MANIFEST;
+ const request = shaka.net.NetworkingEngine.makeRequest(
+ this.manifestUris_, this.config_.retryParameters);
+ const networkingEngine = this.playerInterface_.networkingEngine;
+
+ const format = shaka.util.CmcdManager.StreamingFormat.SMOOTH;
+ this.playerInterface_.modifyManifestRequest(request, {format: format});
+
+ const startTime = Date.now();
+ const operation = networkingEngine.request(requestType, request);
+ this.operationManager_.manage(operation);
+
+ const response = await operation.promise;
+
+ // Detect calls to stop().
+ if (!this.playerInterface_) {
+ return 0;
+ }
+
+ // 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);
+
+ // Let the caller know how long this update took.
+ return 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;
+ }
+ // Star-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);
+
+ 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,
+ 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 = [];
+ if (!audioStreams.length) {
+ for (const videoStream of videoStreams) {
+ const variant = this.createVariant_(
+ /* audioStream= */ null, videoStream);
+ variants.push(variant);
+ }
+ } else if (!videoStreams.length) {
+ for (const audioStream of audioStreams) {
+ const variant = this.createVariant_(
+ audioStream, /* videoStream= */ null);
+ variants.push(variant);
+ }
+ } else {
+ for (const audioStream of audioStreams) {
+ for (const videoStream of videoStreams) {
+ const variant = this.createVariant_(audioStream, videoStream);
+ variants.push(variant);
+ }
+ }
+ }
+ 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);
+ const codecPrivateData =
+ qualityLevel.getAttribute('CodecPrivateData');
+
+ /** @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: codecPrivateData,
+ },
+ };
+
+ 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 (optionnal 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 === null || 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;
+ }
+
+ switch (type) {
+ case 'audio':
+ stream.type = ContentType.AUDIO;
+ // This mimetype is fake to allow the transmuxing.
+ stream.mimeType = 'mss/audio/mp4';
+ stream.codecs = this.getAACCodec_(qualityLevel, fourCCValue);
+ break;
+ case 'video':
+ stream.type = ContentType.VIDEO;
+ // This mimetype is fake to allow the transmuxing.
+ stream.mimeType = 'mss/video/mp4';
+ stream.codecs = this.getH264Codec_(qualityLevel);
+ 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 = () => {
+ 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} fourCCValue
+ * @return {string}
+ * @private
+ */
+ getAACCodec_(qualityLevel, fourCCValue) {
+ const codecPrivateData = qualityLevel.getAttribute('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 === undefined || 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
+ * @return {string}
+ * @private
+ */
+ getH264Codec_(qualityLevel) {
+ const codecPrivateData = qualityLevel.getAttribute('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 '';
+ }
+ // => 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.}
+ */
+ 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_ = 10000000;
+
+
+/**
+ * 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 00000000000..9f8adf2a3d1
--- /dev/null
+++ b/lib/mss/mss_utils.js
@@ -0,0 +1,799 @@
+/*! @license
+ * Shaka Player
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+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;
+ // 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 00000000000..7c2fee5ff69
--- /dev/null
+++ b/lib/transmuxer/mss_transmuxer.js
@@ -0,0 +1,328 @@
+/*! @license
+ * Shaka Player
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+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();
+
+ 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.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');
+ // processTfrf(e.request, tfrf, tfdt, streamProcessor);
+ 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 a544689b59e..798953ca61c 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.
+ */
+ '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 0249c135867..85b7450708e 100644
--- a/lib/util/player_configuration.js
+++ b/lib/util/player_configuration.js
@@ -151,6 +151,20 @@ shaka.util.PlayerConfiguration = class {
liveSegmentsDelay: 3,
sequenceMode: supportsSequenceMode,
},
+ 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 82ea12d27b1..84567cd2606 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
@@ -412,7 +425,7 @@ shaka.util.XmlUtils = class {
*/
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 bd88c986c67..36c760bd2aa 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 19a51fc6b68..323e812cdd2 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 2cbd8398b52..78c00fea643 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 ff954fc45b3..33440cb6029 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 6662816eee7..3626b1f7f29 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 51caa61975b..63851944d6b 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 663646dafb6..c6788eb568e 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 48069772121..cc99561dff9 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_unit.js b/test/mss/mss_parser_unit.js
new file mode 100644
index 00000000000..13342d25842
--- /dev/null
+++ b/test/mss/mss_parser_unit.js
@@ -0,0 +1,293 @@
+/*! @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('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('check the correct segmentIndex', async () => {
+ // const manifestText = [
+ // '',
+ // ' ',
+ // ' ',
+ // ' ',
+ // ' ',
+ // '',
+ // ].join('\n');
+
+ // const baseUri = 'dummy://foo';
+
+ // const references = [
+ // ManifestParser.makeReference('128000/0', 0, 3, baseUri),
+ // ManifestParser.makeReference('128000/30000000', 3, 6, baseUri),
+ // ];
+ // await Mss.testSegmentIndex(manifestText, references);
+ // });
+});
diff --git a/test/test/util/mss_parser_util.js b/test/test/util/mss_parser_util.js
new file mode 100644
index 00000000000..ca5fd14e383
--- /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;
+ }
+};