diff --git a/build/webpack.dev.cjs b/build/webpack.dev.cjs
index e9c605ac29..322ec5e695 100644
--- a/build/webpack.dev.cjs
+++ b/build/webpack.dev.cjs
@@ -19,7 +19,7 @@ const umdDevConfig = merge(umdConfig, {
open: ['samples/index.html'],
hot: true,
compress: true,
- port: 3000
+ port: 3001
}
});
diff --git a/docs/cmcd-v2/reporting-modes.md b/docs/cmcd-v2/reporting-modes.md
new file mode 100644
index 0000000000..392fb56bec
--- /dev/null
+++ b/docs/cmcd-v2/reporting-modes.md
@@ -0,0 +1,17 @@
+## Reporting modes
+
+Here are notes about the challenge of implementing the new reporting modes of CMCD-v2.
+
+### Response Mode
+
+- Modified the `_updateRequestUrlAndHeadersWithCMCD` function to save the cmcdHeaders or `cmcdParams` in `customData` so they can be sent to the remote server in the `_onRequestEnd` function.
+
+- Observations:
+ - The mode that already exists in the configuration for the "transmition mode" might be confusing with the new mode for the "reporting mode".
+
+ - The ‘ts’ key is not entirely accurate since the CMCD parameters are generated before the request starts.
+
+
+### Event Mode
+
+- Should the state be set to rebuffering when the video buffer gets stalled? or also the audio buffer?
\ No newline at end of file
diff --git a/samples/advanced/cmcd-alternative-ads.html b/samples/advanced/cmcd-alternative-ads.html
new file mode 100644
index 0000000000..50d423a91b
--- /dev/null
+++ b/samples/advanced/cmcd-alternative-ads.html
@@ -0,0 +1,198 @@
+
+
+
+
+ Live + AlternativeMPD + ListMPD + CMCD v2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Live + AlternativeMPD + ListMPD + CMCD v2
+
This sample shows how use AlternativeMPD and ListMPD to replace and Ad slot with an Ad Pod while sending all the CMCD v2 requests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/advanced/cmcd.html b/samples/advanced/cmcd.html
index 905211b0fa..ec42cddf50 100644
--- a/samples/advanced/cmcd.html
+++ b/samples/advanced/cmcd.html
@@ -46,11 +46,33 @@
player.updateSettings({
streaming: {
cmcd: {
- enabled: true, /* enable reporting of cmcd parameters */
+ enabled: true, /* global enable reporting of cmcd parameters */
+ version: 2, /* 1 is the default version */
+ reporting: {
+ requestMode: {
+ enabled: true, /* enable cmcdv2 request mode */
+ mode: CMCD_MODE_QUERY, /* overrides global mode */
+ },
+ responseMode: {
+ enabled: true, /* enable cmcdv2 response mode. FALSE by default */
+ mode: CMCD_MODE_QUERY, /* overrides global mode. JSON not supported */
+ enabledKeys: ['sid', 'cid', 'ts', 'url', 'br', 'd', 'ot', 'tb', 'v', 'sta', 'ttfb', 'ttlb','rc'], /* optional, overrides global keys */
+ requestUrl: 'http://localhost:3000/cmcd/response-mode', /*mandatory, URL to send report */
+ requestMethod: 'POST', /*optional, get by default */
+ },
+ eventMode: {
+ enabled: true, /* enable cmcdv2 event mode. FALSE by default */
+ mode: CMCD_MODE_QUERY, /*overrides global mode. JSON not supported */
+ interval: 10000, /* time in ms between regular requests. Default 30s or 0 to omit */
+ enabledKeys: ['sta', 'sid', 'cid', 'sf', 'v', 'ltc', 'msd', 'sta'], /* Currently only supports: 'sta', 'ts', 'sid', 'cid', 'sf', 'v', 'lb', 'pr' */
+ requestUrl: 'http://localhost:3000/cmcd/event-mode', /* mandatory, URL to send report */
+ requestMethod: 'POST', /* optional, get by default */
+ }
+ },
sid: 'b248658d-1d1a-4039-91d0-8c08ba597da5', /* session id send with each request */
cid: '21cf726cfe3d937b5f974f72bb5bd06a', /* content id send with each request */
- mode: CMCD_MODE_QUERY,
- enabledKeys: ['br', 'd', 'ot', 'tb' , 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su' , 'bs', 'rtp' , 'cid', 'pr', 'sf', 'sid', 'st', 'v']
+ mode: CMCD_MODE_QUERY, /* global mode if not specified in each mode */
+ enabledKeys: ['br', 'd', 'ot', 'tb' , 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su' , 'bs', 'rtp' , 'cid', 'pr', 'sf', 'sid', 'st', 'v', 'sta', 'ltc', 'msd', 'sta']
}
}
});
@@ -63,7 +85,14 @@
log('type: ' + event.mediaType);
log('file: ' + event.url.split('/').pop())
var mode = player.getSettings().streaming.cmcd.mode;
- var data = mode === CMCD_MODE_HEADER ? getKeysForHeaderMode(event) : getKeysForQueryMode(event);
+ const cmcdVersion = player.getSettings().streaming.cmcd.version;
+ var data = {};
+ if (cmcdVersion === 1) {
+ data = mode === CMCD_MODE_HEADER ? getKeysForHeaderMode(event) : getKeysForQueryMode(event);
+ } else if (cmcdVersion === 2){
+ /* Currently, headers are always generated in cmcd version 2 */
+ data = getKeysForHeaderMode(event);
+ }
var keys = Object.keys(data);
keys = keys.sort();
for (var key of keys) {
diff --git a/samples/advanced/mpds/ad-pod.mpd b/samples/advanced/mpds/ad-pod.mpd
new file mode 100644
index 0000000000..41220f9ba6
--- /dev/null
+++ b/samples/advanced/mpds/ad-pod.mpd
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/advanced/mpds/live-alternative-ads.mpd b/samples/advanced/mpds/live-alternative-ads.mpd
new file mode 100644
index 0000000000..ead838ceed
--- /dev/null
+++ b/samples/advanced/mpds/live-alternative-ads.mpd
@@ -0,0 +1,49 @@
+
+
+ https://livesim.dashif.org/livesim2/scte35_2/testpic_2s/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/demo.html b/samples/demo.html
new file mode 100644
index 0000000000..d00e8d2463
--- /dev/null
+++ b/samples/demo.html
@@ -0,0 +1,363 @@
+
+
+
+
+ Alternative Config
+
+
+
+
+
+
+
+
+
+
+
+ Add Alternative
+ Add Recommended
+
+
+
+
URI
+
presentationTime(ms)
+
duration(ms)
+
eRTO(ms)
+
mode(insert/replace)
+
returnOffset(ms)
+
+
+
+
+
+
+
+
+
Manifest will appear here...
+
+
+
+
+
+
+
+
+
diff --git a/samples/demo_bu.html b/samples/demo_bu.html
new file mode 100644
index 0000000000..5ae2a761ab
--- /dev/null
+++ b/samples/demo_bu.html
@@ -0,0 +1,268 @@
+
+
+
+
+ Alternative Config
+
+
+
+
+
+
+
+
+
+
+
+ Add Alternative
+ Add Recommended
+
+
+
+
URI
+
presentationTime(ms)
+
duration(ms)
+
eRTO(ms)
+
mode(insert/replace)
+
returnOffset(ms)
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/develop.html b/samples/develop.html
new file mode 100644
index 0000000000..e3ec0e9dac
--- /dev/null
+++ b/samples/develop.html
@@ -0,0 +1,59 @@
+
+
+
+
+ Buffer target
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/samples.json b/samples/samples.json
index 3c8a44999b..a9482e7cc9 100644
--- a/samples/samples.json
+++ b/samples/samples.json
@@ -708,6 +708,17 @@
"Audio"
]
},
+ {
+ "title": "Live + AlternativeMPD + ListMPD + CMCD",
+ "description": "This sample shows how use AlternativeMPD and ListMPD to replace and Ad slot with an Ad Pod while sending all the CMCD v2 requests.",
+ "href": "advanced/cmcd-alternative-ads.html",
+ "image": "lib/img/livesim-1.jpg",
+ "labels": [
+ "Live",
+ "Video",
+ "Audio"
+ ]
+ },
{
"title": "Flexible Insertion of URL Parameters Sample",
"description": "This sample demonstrates the Flexible Insertion of URL Parameters in dash.js.",
diff --git a/src/core/Settings.js b/src/core/Settings.js
index 9a75f9ea35..55681555ae 100644
--- a/src/core/Settings.js
+++ b/src/core/Settings.js
@@ -322,6 +322,9 @@ import Events from './events/Events.js';
* audioChannelConfiguration: 'urn:mpeg:mpegB:cicp:ChannelConfiguration',
* role: 'urn:mpeg:dash:role:2011',
* accessibility: 'urn:mpeg:dash:role:2011'
+ * },
+ * listMpds: {
+ * minEarliestResolutionTimeOffset: 2,
* }
* },
* errors: {
@@ -906,6 +909,12 @@ import Events from './events/Events.js';
* Maximum number of metrics that are persisted per type.
*/
+/**
+ * @typedef {Object} listMpdSettings
+ * @property {boolean} [minEarliestResolutionTimeOffset=2]
+ * Min earliest resolution time offset avalaible.
+ */
+
/**
* @typedef {Object} StreamingSettings
* @property {number} [abandonLoadTimeout=10000]
@@ -1020,6 +1029,8 @@ import Events from './events/Events.js';
* @property {module:Settings~defaultSchemeIdUri} defaultSchemeIdUri
* Default schemeIdUri for descriptor type elements
* These strings are used when not provided with setInitialMediaSettingsFor()
+ * @property {module:Settings~listMpdSettings} listMpd
+ * Settings related to List Mpd configuration
*/
@@ -1319,13 +1330,37 @@ function Settings() {
cmcd: {
applyParametersFromMpd: true,
enabled: false,
+ version: 2,
sid: null,
cid: null,
rtp: null,
+ int: false,
rtpSafetyFactor: 5,
mode: Constants.CMCD_MODE_QUERY,
enabledKeys: Constants.CMCD_AVAILABLE_KEYS,
- includeInRequests: ['segment', 'mpd']
+ includeInRequests: ['segment', 'mpd'],
+ reporting: {
+ requestMode: {
+ enabled: true,
+ mode: null,
+ enabledKeys: null,
+ },
+ responseMode: {
+ enabled: false,
+ mode: null,
+ enabledKeys: null,
+ requestUrl: 'http://localhost:3000/cmcd_server',
+ requestMethod: 'POST'
+ },
+ eventMode: {
+ enabled: false,
+ mode: null,
+ interval: 30000,
+ requestUrl: 'http://localhost:3000/cmcd_server',
+ requestMethod: 'POST',
+ enabledKeys: null
+ }
+ }
},
cmsd: {
enabled: false,
@@ -1339,6 +1374,9 @@ function Settings() {
audioChannelConfiguration: 'urn:mpeg:mpegB:cicp:ChannelConfiguration',
role: 'urn:mpeg:dash:role:2011',
accessibility: 'urn:mpeg:dash:role:2011'
+ },
+ listMpd: {
+ minEarliestResolutionTimeOffset: 2
}
},
errors: {
diff --git a/src/core/events/CoreEvents.js b/src/core/events/CoreEvents.js
index 8bffb0aa56..0d7465d334 100644
--- a/src/core/events/CoreEvents.js
+++ b/src/core/events/CoreEvents.js
@@ -54,7 +54,9 @@ class CoreEvents extends EventsBase {
this.INIT_FRAGMENT_LOADED = 'initFragmentLoaded';
this.INIT_FRAGMENT_NEEDED = 'initFragmentNeeded';
this.INTERNAL_MANIFEST_LOADED = 'internalManifestLoaded';
+ this.ORIGINAL_ALTERNATIVE_MANIFEST_LOADED = 'originalAlternativeManifestLoaded'
this.ORIGINAL_MANIFEST_LOADED = 'originalManifestLoaded';
+ this.IMPORTED_MPDS_LOADED = 'imporedMpdLoaded';
this.LOADING_COMPLETED = 'loadingCompleted';
this.LOADING_PROGRESS = 'loadingProgress';
this.LOADING_DATA_PROGRESS = 'loadingDataProgress';
diff --git a/src/dash/DashAdapter.js b/src/dash/DashAdapter.js
index 1e60068e76..05b909af8f 100644
--- a/src/dash/DashAdapter.js
+++ b/src/dash/DashAdapter.js
@@ -305,10 +305,101 @@ function DashAdapter() {
}
checkConfig();
-
voPeriods = getRegularPeriods(newManifest);
}
+ function mergeManifests(newManifest, importedManifest, periodId, mpdHasDuration) {
+ const periodIndex = newManifest.Period.findIndex(period => period.id === periodId);
+ // The imported manifest should be a single period manifest
+ let newPeriod = {};
+ const linkedPeriod = newManifest.Period[periodIndex];
+ if (importedManifest) {
+ const importedPeriod = importedManifest.Period[0]
+
+ if (importedManifest.hasOwnProperty(DashConstants.PROFILES)) {
+ importedPeriod.profiles = importedManifest.profiles;
+ }
+ if (importedManifest.hasOwnProperty(DashConstants.SUPPLEMENTAL_PROPERTY)) {
+ importedPeriod.SupplementalProperty.concat(importedManifest.SupplementalProperty);
+ }
+ if (importedManifest.hasOwnProperty(DashConstants.ESSENTIAL_PROPERTY)) {
+ importedPeriod.EssentialProperty.concat(importedManifest.EssentialProperty);
+ }
+ if (importedManifest.hasOwnProperty(DashConstants.PROGRAM_INFORMATION)) {
+ newManifest.ProgramInformation = importedManifest.ProgramInformation;
+ }
+
+ newPeriod = {
+ baseUri: importedManifest.baseUri,
+ minBufferTime: importedManifest.minBufferTime,
+ start: linkedPeriod.start ?? importedPeriod.start,
+ id: linkedPeriod.id ?? importedPeriod.id,
+ duration: linkedPeriod.duration,
+ ServiceDescription: linkedPeriod.ServiceDescription || [],
+ SupplementalProperty: linkedPeriod.SupplementalProperty || [],
+ EssentialProperty: linkedPeriod.EssentialProperty || [],
+ EventStream: linkedPeriod.EventStream || [],
+ };
+ if (!mpdHasDuration && importedPeriod.duration && importedPeriod.duration < linkedPeriod.duration) {
+ newPeriod.duration = importedPeriod.duration;
+ newManifest.mediaPresentationDuration += newPeriod.duration;
+ if (linkedPeriod.duration) {
+ newManifest.mediaPresentationDuration -= linkedPeriod.duration;
+ }
+ }
+ const propertiesFromOtherNamespaces = Object.keys(linkedPeriod).filter(function (name) {return name.includes(':')});
+ Object.assign(newPeriod, newPeriod, propertiesFromOtherNamespaces);
+
+ for (let item in importedPeriod.ServiceDescription) {
+ const index = newPeriod.ServiceDescription.findIndex(sd => sd.id === item.id);
+ index != -1 ? newPeriod.ServiceDescription[index] = item : newPeriod.ServiceDescription.push(item);
+ }
+ if (newPeriod.ServiceDescription.length === 0) {
+ delete newPeriod.ServiceDescription;
+ }
+ for (let item in importedPeriod.SupplementalProperty) {
+ const index = newPeriod.SupplementalProperty.findIndex(sp => sp.schemeIdUri === item.schemeIdUri && sp.value === item.value);
+ index != -1 ? newPeriod.SupplementalProperty[index] = item : newPeriod.SupplementalProperty.push(item);
+ }
+ if (newPeriod.SupplementalProperty.length === 0) {
+ delete newPeriod.SupplementalProperty;
+ }
+ for (let item in importedPeriod.EssentialProperty) {
+ const index = newPeriod.EssentialProperty.findIndex(ep => ep.schemeIdUri === item.schemeIdUri && ep.value === item.value);
+ index != -1 ? newPeriod.EssentialProperty[index] = item : newPeriod.EssentialProperty.push(item);
+ }
+ if (newPeriod.EssentialProperty.length === 0) {
+ delete newPeriod.EssentialProperty;
+ }
+ for (let item in importedPeriod.EventStream) {
+ const index = newPeriod.EventStream.findIndex(es => es.schemeIdUri === item.schemeIdUri && es.value === item.value);
+ index != -1 ? newPeriod.EventStream[index] = item : newPeriod.EventStream.push(item);
+ }
+ if (newPeriod.EventStream.length === 0) {
+ delete newPeriod.EventStream;
+ }
+ newPeriod.baseURL = importedManifest.baseUri;
+ if (importedManifest.BaseURL) {
+ const baseUrls = importedManifest.BaseURL.concat(importedPeriod.BaseURL)
+ .filter(function( element ) {
+ return element !== undefined;
+ });
+ newPeriod.BaseURL = baseUrls;
+ }
+
+ newPeriod.AdaptationSet = importedPeriod.AdaptationSet;
+ } else {
+ newPeriod = linkedPeriod;
+ delete newPeriod.ImportedMPD;
+ delete newPeriod.earliestResolutionTimeOffset;
+ }
+ if (newPeriod.minBufferTime && (newPeriod.AdaptationSet || newPeriod.duration === 0)) {
+ newManifest.Period[periodIndex] = newPeriod;
+ } else {
+ newManifest.Period.splice(periodIndex, 1);
+ }
+ }
+
/**
* Returns an array of streamInfo objects
* @param {object} externalManifest
@@ -327,7 +418,6 @@ function DashAdapter() {
checkConfig();
voLocalPeriods = getRegularPeriods(externalManifest);
}
-
if (voLocalPeriods.length > 0) {
if (!maxStreamsInfo || maxStreamsInfo > voLocalPeriods.length) {
maxStreamsInfo = voLocalPeriods.length;
@@ -614,6 +704,18 @@ function DashAdapter() {
return dashManifestModel.getDuration(manifest);
}
+ /**
+ * Returns all linked periods of the MPD
+ * @param {object} externalManifest Omit this value if no external manifest should be used
+ * @returns {Array} linked periods
+ * @memberOf module:DashAdapter
+ * @instance
+ */
+ function getLinkPeriods(externalManifest) {
+ const mpd = getMpd(externalManifest);
+ return dashManifestModel.getLinkPeriods(mpd);
+ }
+
/**
* Returns all periods of the MPD
* @param {object} externalManifest Omit this value if no external manifest should be used
@@ -1232,6 +1334,7 @@ function DashAdapter() {
getIsTextTrack,
getIsTypeOf,
getLocation,
+ getLinkPeriods,
getMainAdaptationForType,
getManifestUpdatePeriod,
getMediaInfoForType,
@@ -1250,6 +1353,7 @@ function DashAdapter() {
getUTCTimingSources,
getVoRepresentations,
isPatchValid,
+ mergeManifests,
reset,
setConfig,
updatePeriods,
diff --git a/src/dash/constants/DashConstants.js b/src/dash/constants/DashConstants.js
index 6074ac2ba5..d4e29c628f 100644
--- a/src/dash/constants/DashConstants.js
+++ b/src/dash/constants/DashConstants.js
@@ -39,6 +39,8 @@ export default {
ADAPTATION_SETS: 'adaptationSets',
ADAPTATION_SET_SWITCHING_SCHEME_ID_URI: 'urn:mpeg:dash:adaptation-set-switching:2016',
ADD: 'add',
+ ALTERNATIVE_MPD: 'AlternativeMPD',
+ ALTERNATIVE_MPD_SCHEME_ID: 'urn:mpeg:dash:event:alternativeMPD:2022',
ASSET_IDENTIFIER: 'AssetIdentifier',
AUDIO_CHANNEL_CONFIGURATION: 'AudioChannelConfiguration',
AUDIO_SAMPLING_RATE: 'audioSamplingRate',
@@ -103,6 +105,7 @@ export default {
LA_URL_LOWER_CASE: 'laurl',
LABEL: 'Label',
LANG: 'lang',
+ LIST_PROFILE_SCHEME: 'urn:mpeg:dash:profile:list:2024',
LOCATION: 'Location',
MAIN: 'main',
MAXIMUM_SAP_PERIOD: 'maximumSAPPeriod',
@@ -120,6 +123,7 @@ export default {
MIN_BUFFER_TIME: 'minBufferTime',
MP4_PROTECTION_SCHEME: 'urn:mpeg:dash:mp4protection:2011',
MPD: 'MPD',
+ MPD_LIST: 'list',
MPD_TYPE: 'mpd',
MPD_PATCH_TYPE: 'mpdpatch',
ORIGINAL_MPD_ID: 'mpdId',
@@ -136,6 +140,7 @@ export default {
APPLICATION: 'application'
},
PROFILES: 'profiles',
+ PROGRAM_INFORMATION: 'ProgramInformation',
PSSH: 'pssh',
PUBLISH_TIME: 'publishTime',
QUALITY_RANKING : 'qualityRanking',
diff --git a/src/dash/models/DashManifestModel.js b/src/dash/models/DashManifestModel.js
index d49b35c9c4..e220bc2604 100644
--- a/src/dash/models/DashManifestModel.js
+++ b/src/dash/models/DashManifestModel.js
@@ -50,6 +50,7 @@ import PatchLocation from '../vo/PatchLocation.js';
import Period from '../vo/Period.js';
import ProducerReferenceTime from '../vo/ProducerReferenceTime.js';
import Representation from '../vo/Representation.js';
+import AlternativeMpd from '../vo/AlternativeMpd.js';
import URLUtils from '../../streaming/utils/URLUtils.js';
import UTCTiming from '../vo/UTCTiming.js';
import Utils from '../../core/Utils.js';
@@ -364,10 +365,11 @@ function DashManifestModel() {
let i,
len;
const adaptations = [];
-
- for (i = 0, len = realAdaptations.length; i < len; i++) {
- if (getIsTypeOf(realAdaptations[i], type)) {
- adaptations.push(processAdaptation(realAdaptations[i]));
+ if (realAdaptations) {
+ for (i = 0, len = realAdaptations.length; i < len; i++) {
+ if (getIsTypeOf(realAdaptations[i], type)) {
+ adaptations.push(processAdaptation(realAdaptations[i]));
+ }
}
}
@@ -861,7 +863,6 @@ function DashManifestModel() {
for (i = 0, len = mpd && mpd.manifest && mpd.manifest.Period ? mpd.manifest.Period.length : 0; i < len; i++) {
realPeriod = mpd.manifest.Period[i];
-
// If the attribute @start is present in the Period, then the
// Period is a regular Period and the PeriodStart is equal
// to the value of this attribute.
@@ -928,6 +929,34 @@ function DashManifestModel() {
return voPeriods;
}
+ function getLinkPeriods(mpd) {
+ const linkedPeriods = []
+
+ if (!mpd || !mpd.manifest || !mpd.manifest.Period) {
+ return linkedPeriods;
+ }
+
+ if (mpd.availabilityEndTime < Date.now()) {
+ throw new Error('availabilityEndTime must be greater than current time');
+ }
+
+ let currentPeriod = null;
+ for (let i = 0, len = mpd.manifest.Period.length; i < len; i++) {
+ currentPeriod = mpd.manifest.Period[i];
+ if (currentPeriod.ImportedMPD) {
+ linkedPeriods.push(currentPeriod);
+ }
+ }
+
+ if (linkedPeriods.length > 0) {
+ if (mpd.manifest.type !== DashConstants.MPD_LIST) {
+ throw new Error('Linked periods are only allowed in a list MPD');
+ }
+ }
+
+ return linkedPeriods
+ }
+
function getPeriodId(realPeriod, i) {
if (!realPeriod) {
throw new Error('Period cannot be null or undefined');
@@ -1061,6 +1090,13 @@ function DashManifestModel() {
event.id = null;
}
+ if (currentMpdEvent.hasOwnProperty(DashConstants.ALTERNATIVE_MPD)) {
+ event.alternativeMpd = getAlternativeMpd(currentMpdEvent.AlternativeMPD);
+ event.calculatedPresentationTime = 0;
+ } else {
+ event.alternativeMpd = null;
+ }
+
if (currentMpdEvent.Signal && currentMpdEvent.Signal.Binary) {
// toString is used to manage both regular and namespaced tags
event.messageData = BASE64.decodeArray(currentMpdEvent.Signal.Binary.toString());
@@ -1083,6 +1119,18 @@ function DashManifestModel() {
return events;
}
+ function getAlternativeMpd(event) {
+ const alternativeMpd = new AlternativeMpd();
+ alternativeMpd.uri = event.uri ?? null;
+ alternativeMpd.duration = event.duration ?? null;
+ alternativeMpd.earliestResolutionTimeOffset = event.earliestResolutionTimeOffset / 1000 ?? null;
+ alternativeMpd.mode = event.mode ?? null;
+ alternativeMpd.disableJumpTimeOffest = event.disableJumpTimeOffest ?? null;
+ alternativeMpd.playTimes = event.playTimes ?? null;
+ alternativeMpd.returnOffset = event.returnOffset ?? null;
+ return alternativeMpd;
+ }
+
function getEventStreams(inbandStreams, representation, period) {
const eventStreams = [];
let i;
@@ -1497,6 +1545,7 @@ function DashManifestModel() {
getIsTypeOf,
getLabelsForAdaptation,
getLanguageForAdaptation,
+ getLinkPeriods,
getLocation,
getManifestUpdatePeriod,
getMimeType,
diff --git a/src/dash/vo/AlternativeMpd.js b/src/dash/vo/AlternativeMpd.js
new file mode 100644
index 0000000000..515f56df14
--- /dev/null
+++ b/src/dash/vo/AlternativeMpd.js
@@ -0,0 +1,49 @@
+/**
+ * The copyright in this software is being made available under the BSD License,
+ * included below. This software may be subject to other third party and contributor
+ * rights, including patent rights, and no such rights are granted under this license.
+ *
+ * Copyright (c) 2013, Dash Industry Forum.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * * Neither the name of Dash Industry Forum nor the names of its
+ * contributors may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+/**
+ * @class
+ * @ignore
+ */
+
+class AlternativeMpd {
+ constructor() {
+ this.uri = '';
+ this.earliestResolutionTimeOffset = NaN;
+ this.mode = '';
+ this.disableJumpTimeOffest = NaN;
+ this.playTimes = '';
+ this.presentationTime = NaN;
+ this.duration = NaN;
+ this.returnOffset = NaN;
+ }
+}
+
+export default AlternativeMpd;
diff --git a/src/dash/vo/Event.js b/src/dash/vo/Event.js
index 28e40c7eba..234007091d 100644
--- a/src/dash/vo/Event.js
+++ b/src/dash/vo/Event.js
@@ -42,7 +42,8 @@ class Event {
this.eventStream = null;
this.presentationTimeDelta = NaN; // Specific EMSG Box parameter
this.parsedMessageData = null; // Parsed value of the event message
+ this.alternativeMpd = null;
}
}
-export default Event;
\ No newline at end of file
+export default Event;
diff --git a/src/streaming/ManifestLoader.js b/src/streaming/ManifestLoader.js
index 94b69b6876..c88262399a 100644
--- a/src/streaming/ManifestLoader.js
+++ b/src/streaming/ManifestLoader.js
@@ -42,25 +42,30 @@ import Errors from '../core/errors/Errors.js';
import FactoryMaker from '../core/FactoryMaker.js';
import DashParser from '../dash/parser/DashParser.js';
-function ManifestLoader(config) {
-
- config = config || {};
- const context = this.context;
- const debug = config.debug;
- const settings = config.settings;
- const eventBus = EventBus(context).getInstance();
- const urlUtils = URLUtils(context).getInstance();
-
+function ManifestLoader() {
let instance,
logger,
+ settings,
+ urlUtils,
urlLoader,
+ mssHandler,
+ errHandler,
xlinkController,
+ eventBus,
+ debug,
parser;
- let mssHandler = config.mssHandler;
- let errHandler = config.errHandler;
+ const context = this.context;
+
+ function setConfig(config) {
+ config = config || {};
+ debug = config.debug;
+ settings = config.settings;
+ eventBus = EventBus(context).getInstance();
+ urlUtils = URLUtils(context).getInstance();
+ mssHandler = config.mssHandler;
+ errHandler = config.errHandler;
- function setup() {
logger = debug.getLogger(instance);
eventBus.on(Events.XLINK_READY, onXlinkReady, instance);
@@ -81,7 +86,9 @@ function ManifestLoader(config) {
mediaPlayerModel: config.mediaPlayerModel,
settings: config.settings
});
+ }
+ function setup() {
parser = null;
}
@@ -107,7 +114,7 @@ function ManifestLoader(config) {
}
}
- function load(url, serviceLocation = null, queryParams = null) {
+ function load(url, serviceLocation = null, queryParams = null, linkPeriod = null, alternative = false) {
const requestStartDate = new Date();
const request = new TextRequest(url, HTTPRequest.MPD_TYPE);
@@ -124,129 +131,149 @@ function ManifestLoader(config) {
request.startDate = requestStartDate;
}
- eventBus.trigger(
- Events.MANIFEST_LOADING_STARTED, {
- request
- }
- );
-
- urlLoader.load({
- request: request,
- success: function (data, textStatus, responseURL) {
- // Manage situations in which success is called after calling reset
- if (!xlinkController) {
- return;
- }
-
- let actualUrl,
- baseUri,
- manifest;
-
- // Handle redirects for the MPD - as per RFC3986 Section 5.1.3
- // also handily resolves relative MPD URLs to absolute
- if (responseURL && responseURL !== url) {
- baseUri = urlUtils.parseBaseUrl(responseURL);
- actualUrl = responseURL;
- } else {
- // usually this case will be caught and resolved by
- // responseURL above but it is not available for IE11 and Edge/12 and Edge/13
- // baseUri must be absolute for BaseURL resolution later
- if (urlUtils.isRelative(url)) {
- url = urlUtils.resolve(url, window.location.href);
+ function createUrlLoaderObject(resolve, reject) {
+ return {
+ request: request,
+ success: function (data, textStatus, responseURL) {
+ Events.MANIFEST_LOADING_STARTED, {
+ request
+ }
+ // Manage situations in which success is called after calling reset
+ if (!xlinkController) {
+ return;
}
- baseUri = urlUtils.parseBaseUrl(url);
- }
-
- // A response of no content implies in-memory is properly up to date
- if (textStatus == 'No Content') {
- eventBus.trigger(
- Events.INTERNAL_MANIFEST_LOADED, {
- manifest: null
+ let actualUrl,
+ baseUri,
+ manifest;
+
+ // Handle redirects for the MPD - as per RFC3986 Section 5.1.3
+ // also handily resolves relative MPD URLs to absolute
+ if (responseURL && responseURL !== url) {
+ baseUri = urlUtils.parseBaseUrl(responseURL);
+ actualUrl = responseURL;
+ } else {
+ // usually this case will be caught and resolved by
+ // responseURL above but it is not available for IE11 and Edge/12 and Edge/13
+ // baseUri must be absolute for BaseURL resolution later
+ if (urlUtils.isRelative(url)) {
+ url = urlUtils.resolve(url, window.location.href);
}
- );
- return;
- }
-
- // Create parser according to manifest type
- if (parser === null) {
- parser = createParser(data);
- }
-
- if (parser === null) {
- eventBus.trigger(Events.INTERNAL_MANIFEST_LOADED, {
- manifest: null,
- error: new DashJSError(
- Errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_CODE,
- Errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_MESSAGE + `${url}`
- )
- });
- return;
- }
- // init xlinkcontroller with created parser
- xlinkController.setParser(parser);
+ baseUri = urlUtils.parseBaseUrl(url);
+ }
- try {
- manifest = parser.parse(data);
- } catch (e) {
- eventBus.trigger(Events.INTERNAL_MANIFEST_LOADED, {
- manifest: null,
- error: new DashJSError(
- Errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_CODE,
- Errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_MESSAGE + `${url}`
- )
- });
- return;
- }
+ // A response of no content implies in-memory is properly up to date
+ if (textStatus == 'No Content') {
+ eventBus.trigger(
+ Events.INTERNAL_MANIFEST_LOADED, {
+ manifest: null
+ }
+ );
+ return;
+ }
- if (manifest) {
- manifest.url = actualUrl || url;
+ // Create parser according to manifest type
+ if (parser === null) {
+ parser = createParser(data);
+ }
- // URL from which the MPD was originally retrieved (MPD updates will not change this value)
- if (!manifest.originalUrl) {
- manifest.originalUrl = manifest.url;
+ if (parser === null && !linkPeriod) {
+ eventBus.trigger(Events.INTERNAL_MANIFEST_LOADED, {
+ manifest: null,
+ error: new DashJSError(
+ Errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_CODE,
+ Errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_MESSAGE + `${url}`
+ )
+ });
+ return;
}
- // If there is a mismatch between the manifest's specified duration and the total duration of all periods,
- // and the specified duration is greater than the total duration of all periods,
- // overwrite the manifest's duration attribute. This is a patch for if a manifest is generated incorrectly.
- if (settings &&
- settings.get().streaming.enableManifestDurationMismatchFix &&
- manifest.mediaPresentationDuration &&
- manifest.Period.length > 1) {
- const sumPeriodDurations = manifest.Period.reduce((totalDuration, period) => totalDuration + period.duration, 0);
- if (!isNaN(sumPeriodDurations) && manifest.mediaPresentationDuration > sumPeriodDurations) {
- logger.warn('Media presentation duration greater than duration of all periods. Setting duration to total period duration');
- manifest.mediaPresentationDuration = sumPeriodDurations;
+ // init xlinkcontroller with created parser
+ xlinkController.setParser(parser);
+
+ try {
+ manifest = parser.parse(data);
+ } catch (e) {
+ if (!linkPeriod) {
+ eventBus.trigger(Events.INTERNAL_MANIFEST_LOADED, {
+ manifest: null,
+ error: new DashJSError(
+ Errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_CODE,
+ Errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_MESSAGE + `${url}`
+ )
+ });
}
+ return;
}
- manifest.baseUri = baseUri;
- manifest.loadedTime = new Date();
- xlinkController.resolveManifestOnLoad(manifest);
+ if (manifest) {
+ manifest.url = actualUrl || url;
- eventBus.trigger(Events.ORIGINAL_MANIFEST_LOADED, { originalManifest: data });
- } else {
+ // URL from which the MPD was originally retrieved (MPD updates will not change this value)
+ if (!manifest.originalUrl) {
+ manifest.originalUrl = manifest.url;
+ }
+
+ // If there is a mismatch between the manifest's specified duration and the total duration of all periods,
+ // and the specified duration is greater than the total duration of all periods,
+ // overwrite the manifest's duration attribute. This is a patch for if a manifest is generated incorrectly.
+ if (settings &&
+ settings.get().streaming.enableManifestDurationMismatchFix &&
+ manifest.mediaPresentationDuration &&
+ manifest.Period.length > 1) {
+ const sumPeriodDurations = manifest.Period.reduce((totalDuration, period) => totalDuration + period.duration, 0);
+ if (!isNaN(sumPeriodDurations) && manifest.mediaPresentationDuration > sumPeriodDurations) {
+ logger.warn('Media presentation duration greater than duration of all periods. Setting duration to total period duration');
+ manifest.mediaPresentationDuration = sumPeriodDurations;
+ }
+ }
+
+ manifest.baseUri = baseUri;
+ manifest.loadedTime = new Date();
+ xlinkController.resolveManifestOnLoad(manifest);
+
+ if (linkPeriod) {
+ //TODO: Change name of the event.
+ eventBus.trigger(Events.ORIGINAL_MANIFEST_LOADED, { originalManifest: data });
+ resolve(manifest)
+ } else if (alternative) {
+ eventBus.trigger(Events.ORIGINAL_ALTERNATIVE_MANIFEST_LOADED, { manifest: data });
+ resolve(manifest)
+ } else {
+ eventBus.trigger(Events.ORIGINAL_MANIFEST_LOADED, { originalManifest: data });
+ resolve(manifest);
+ }
+ } else if (!linkPeriod && !alternative) {
+ eventBus.trigger(Events.INTERNAL_MANIFEST_LOADED, {
+ manifest: null,
+ error: new DashJSError(
+ Errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_CODE,
+ Errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_MESSAGE + `${url}`
+ )
+ });
+ }
+ },
+
+ error: function (request, statusText, errorText) {
+ if (linkPeriod){
+ reject();
+ }
eventBus.trigger(Events.INTERNAL_MANIFEST_LOADED, {
manifest: null,
error: new DashJSError(
- Errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_CODE,
- Errors.MANIFEST_LOADER_PARSING_FAILURE_ERROR_MESSAGE + `${url}`
+ Errors.MANIFEST_LOADER_LOADING_FAILURE_ERROR_CODE,
+ Errors.MANIFEST_LOADER_LOADING_FAILURE_ERROR_MESSAGE + `${url}, ${errorText}`
)
- });
+ });
}
- },
- error: function (request, statusText, errorText) {
- eventBus.trigger(Events.INTERNAL_MANIFEST_LOADED, {
- manifest: null,
- error: new DashJSError(
- Errors.MANIFEST_LOADER_LOADING_FAILURE_ERROR_CODE,
- Errors.MANIFEST_LOADER_LOADING_FAILURE_ERROR_MESSAGE + `${url}, ${errorText}`
- )
- });
}
+ }
+
+ return new Promise((resolve, reject) => {
+ urlLoader.load(createUrlLoaderObject(resolve, reject));
});
+
}
function reset() {
@@ -269,7 +296,8 @@ function ManifestLoader(config) {
instance = {
load: load,
- reset: reset
+ reset: reset,
+ setConfig
};
setup();
@@ -279,5 +307,5 @@ function ManifestLoader(config) {
ManifestLoader.__dashjs_factory_name = 'ManifestLoader';
-const factory = FactoryMaker.getClassFactory(ManifestLoader);
+const factory = FactoryMaker.getSingletonFactory(ManifestLoader);
export default factory;
diff --git a/src/streaming/ManifestUpdater.js b/src/streaming/ManifestUpdater.js
index 3d2edf5f97..812ba52964 100644
--- a/src/streaming/ManifestUpdater.js
+++ b/src/streaming/ManifestUpdater.js
@@ -254,8 +254,14 @@ function ManifestUpdater() {
if (refreshDelay * 1000 > 0x7FFFFFFF) {
refreshDelay = 0x7FFFFFFF / 1000;
}
- eventBus.trigger(Events.MANIFEST_UPDATED, { manifest: manifest });
- logger.info('Manifest has been refreshed at ' + date + '[' + date.getTime() / 1000 + '] ');
+
+ if (manifest.profiles === DashConstants.LIST_PROFILE_SCHEME) {
+ const linkedPeriods = adapter.getLinkPeriods(manifest)
+ eventBus.trigger(Events.IMPORTED_MPDS_LOADED, { manifest, linkedPeriods } )
+ } else {
+ eventBus.trigger(Events.MANIFEST_UPDATED, { manifest: manifest });
+ logger.info('Manifest has been refreshed at ' + date + '[' + date.getTime() / 1000 + '] ');
+ }
if (!isPaused) {
startManifestRefreshTimer();
diff --git a/src/streaming/MediaPlayer.js b/src/streaming/MediaPlayer.js
index e212ce0cc6..b1ea7d23b6 100644
--- a/src/streaming/MediaPlayer.js
+++ b/src/streaming/MediaPlayer.js
@@ -31,6 +31,7 @@
import {Cta608Parser} from '@svta/common-media-library/cta/608/Cta608Parser';
import Constants from './constants/Constants.js';
import DashConstants from '../dash/constants/DashConstants.js';
+import AlternativeMpdController from './controllers/AlternativeMpdController.js';
import MetricsConstants from './constants/MetricsConstants.js';
import PlaybackController from './controllers/PlaybackController.js';
import StreamController from './controllers/StreamController.js';
@@ -81,6 +82,7 @@ import TextController from './text/TextController.js';
import CustomParametersModel from './models/CustomParametersModel.js';
import ThroughputController from './controllers/ThroughputController.js';
import ClientDataReportingController from './controllers/ClientDataReportingController.js';
+import ListMpdController from './controllers/ListMpdController.js';
/**
* The media types
@@ -143,6 +145,7 @@ function MediaPlayer() {
throughputController,
schemeLoaderFactory,
timelineConverter,
+ alternativeMpdController,
mediaController,
protectionController,
metricsReportingController,
@@ -162,6 +165,7 @@ function MediaPlayer() {
serviceDescriptionController,
contentSteeringController,
catchupController,
+ listMpdController,
dashMetrics,
manifestModel,
cmcdModel,
@@ -224,6 +228,9 @@ function MediaPlayer() {
if (config.gapController) {
gapController = config.gapController;
}
+ if (config.alternativeMpdController) {
+ alternativeMpdController = config.alternativeMpdController;
+ }
if (config.throughputController) {
throughputController = config.throughputController
}
@@ -286,7 +293,7 @@ function MediaPlayer() {
* @memberof module:MediaPlayer
* @instance
*/
- function initialize(view, source, autoPlay, startTime = NaN) {
+ function initialize(view, source, autoPlay, startTime = NaN, alternativeContext = null) {
if (!capabilities) {
capabilities = Capabilities(context).getInstance();
capabilities.setConfig({
@@ -320,6 +327,10 @@ function MediaPlayer() {
schemeLoaderFactory = SchemeLoaderFactory(context).getInstance();
}
+ if (!alternativeMpdController) {
+ alternativeMpdController = AlternativeMpdController(alternativeContext ? alternativeContext : context).getInstance();
+ }
+
if (!playbackController) {
playbackController = PlaybackController(context).getInstance();
}
@@ -352,6 +363,10 @@ function MediaPlayer() {
capabilitiesFilter = CapabilitiesFilter(context).getInstance();
}
+ if (!listMpdController) {
+ listMpdController = ListMpdController(context).getInstance();
+ }
+
adapter = DashAdapter(context).getInstance();
manifestModel = ManifestModel(context).getInstance();
@@ -390,6 +405,15 @@ function MediaPlayer() {
adapter
});
+ alternativeMpdController.setConfig({
+ videoModel,
+ manifestModel,
+ DashConstants,
+ mediaPlayerFactory: FactoryMaker.getClassFactory(MediaPlayer)(),
+ playbackController,
+ alternativeContext: context
+ });
+
if (!segmentBaseController) {
segmentBaseController = SegmentBaseController(context).getInstance({
dashMetrics: dashMetrics,
@@ -429,6 +453,13 @@ function MediaPlayer() {
eventBus
})
+ listMpdController.setConfig({
+ settings: settings,
+ dashAdapter: adapter
+ });
+
+ listMpdController.initialize();
+
restoreDefaultUTCTimingSources();
setAutoPlay(autoPlay !== undefined ? autoPlay : true);
@@ -457,8 +488,12 @@ function MediaPlayer() {
* @memberof module:MediaPlayer
* @instance
*/
- function reset() {
- attachSource(null);
+ function reset(onlyControllers) {
+
+ if (!onlyControllers) {
+ attachSource(null);
+ }
+
attachView(null);
protectionData = null;
if (protectionController) {
@@ -585,12 +620,14 @@ function MediaPlayer() {
* @throws {@link module:MediaPlayer~SOURCE_NOT_ATTACHED_ERROR SOURCE_NOT_ATTACHED_ERROR} if called before attachSource function
* @instance
*/
- function preload() {
- if (videoModel.getElement() || streamingInitialized) {
+ function preload(time) {
+ if (videoModel.getElement() || (streamingInitialized && !time)) {
return;
}
if (source) {
- _initializePlayback(providedStartTime);
+ const playbackTime = time ? time : providedStartTime;
+ console.log(playbackTime)
+ _initializePlayback(playbackTime);
} else {
throw SOURCE_NOT_ATTACHED_ERROR;
}
@@ -1454,6 +1491,7 @@ function MediaPlayer() {
if (playbackInitialized) { //Reset if we have been playing before, so this is a new element.
_resetPlaybackControllers();
}
+ console.log(providedStartTime);
_initializePlayback(providedStartTime);
}
@@ -2482,6 +2520,7 @@ function MediaPlayer() {
textController.initialize();
gapController.initialize();
catchupController.initialize();
+ alternativeMpdController.initialize();
cmcdModel.initialize();
cmsdModel.initialize();
contentSteeringController.initialize();
@@ -2489,7 +2528,8 @@ function MediaPlayer() {
}
function _createManifestLoader() {
- return ManifestLoader(context).create({
+ const manifestLoader = ManifestLoader(context).getInstance();
+ manifestLoader.setConfig({
debug: debug,
errHandler: errHandler,
dashMetrics: dashMetrics,
@@ -2497,6 +2537,7 @@ function MediaPlayer() {
mssHandler: mssHandler,
settings: settings
});
+ return manifestLoader
}
function _detectProtection() {
diff --git a/src/streaming/MediaPlayerEvents.js b/src/streaming/MediaPlayerEvents.js
index c6b39b6ee3..940b3fe536 100644
--- a/src/streaming/MediaPlayerEvents.js
+++ b/src/streaming/MediaPlayerEvents.js
@@ -156,6 +156,25 @@ class MediaPlayerEvents extends EventsBase {
* @event MediaPlayerEvents#MANIFEST_LOADED
*/
this.MANIFEST_LOADED = 'manifestLoaded';
+ /**
+ * Triggered when TBD
+ * @event MediaPlayerEvents#ALTERNATIVE_MANIFEST_LOADED
+ */
+ this.ALTERNATIVE_MANIFEST_LOADED = 'alternativeManifestLoaded'
+
+ /**
+ * Sent when the alternative media begins to play (either for the first time, after having been paused,
+ * or after ending and then restarting).
+ *
+ * @event MediaPlayerEvents#ALTERNATIVE_PLAYBACK_PLAYING
+ */
+ this.ALTERNATIVE_PLAYBACK_PLAYING = 'alternativePlaybackPlaying';
+
+ /**
+ * Sent when alternative playback completes.
+ * @event MediaPlayerEvents#ALTERNATIVE_PLAYBACK_ENDED
+ */
+ this.ALTERNATIVE_PLAYBACK_ENDED = 'alternativePlaybackEnded';
/**
* Triggered anytime there is a change to the overall metrics.
diff --git a/src/streaming/Stream.js b/src/streaming/Stream.js
index dad44879c5..8a529106b0 100644
--- a/src/streaming/Stream.js
+++ b/src/streaming/Stream.js
@@ -608,6 +608,35 @@ function Stream(config) {
trackChangedEvents = [];
}
+ function resetAlt(keepBuffers) {
+
+ if (fragmentController) {
+ fragmentController.reset();
+ fragmentController = null;
+ }
+
+ if (abrController && streamInfo) {
+ abrController.clearDataForStream(streamInfo.id);
+ }
+
+ if (segmentBlacklistController) {
+ segmentBlacklistController.reset();
+ segmentBlacklistController = null;
+ }
+
+ console.log('resetAlt, keepBuffers', keepBuffers);
+
+
+ resetInitialSettings(keepBuffers);
+
+ streamInfo = null;
+
+ unRegisterEvents();
+
+ unRegisterProtectionEvents();
+
+ }
+
function reset(keepBuffers) {
if (fragmentController) {
@@ -1083,6 +1112,7 @@ function Stream(config) {
prepareQualityChange,
prepareTrackChange,
reset,
+ resetAlt,
setIsEndedEventSignaled,
setMediaSource,
startPreloading,
diff --git a/src/streaming/constants/Constants.js b/src/streaming/constants/Constants.js
index a95bc2d6c0..c0d88e3937 100644
--- a/src/streaming/constants/Constants.js
+++ b/src/streaming/constants/Constants.js
@@ -220,7 +220,16 @@ export default {
* @memberof Constants#
* @static
*/
- CMCD_AVAILABLE_KEYS: ['br', 'd', 'ot', 'tb', 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su', 'bs', 'rtp', 'cid', 'pr', 'sf', 'sid', 'st', 'v'],
+ CMCD_AVAILABLE_KEYS: ['e', 'bg', 'int', 'br', 'd', 'ot', 'tb', 'bl', 'dl', 'mtp', 'nor', 'nrr', 'su', 'bs', 'rtp', 'cid', 'pr', 'sf', 'sid', 'st', 'v', 'ts', 'url', 'sta', 'ltc', 'msd'],
+
+ /**
+ * @constant {string} CMCD_AVAILABLE_KEYS_EVENT specifies all the availables keys for the Event Mode of CMCD v2.
+ * @memberof Constants#
+ * @static
+ * TODO: Confirm keys and create CMCD AVAILABLE KEYS arrays for CMCD v2 and Response Mode
+ * TODO: Add support for more keys
+ */
+ CMCD_AVAILABLE_KEYS_EVENT: ['bg', 'br', 'bs', 'bl', 'mtp', 'sf', 'e', 'int', 'sta', 'ts', 'sid', 'cid', 'sf', 'v', 'lb', 'pr', 'ltc', 'msd'],
/**
* @constant {string} CMCD_AVAILABLE_REQUESTS specifies all the availables requests type for CMCD metrics.
@@ -325,6 +334,13 @@ export default {
ABANDON_FRAGMENT_RULES: {
ABANDON_REQUEST_RULE: 'AbandonRequestsRule'
},
+ ALTERNATIVE_MPD: {
+ MODES: {
+ REPLACE: 'replace',
+ INSERT: 'insert'
+ },
+ URI: 'urn:mpeg:dash:event:alternativeMPD:2022'
+ },
/**
* @constant {string} ID3_SCHEME_ID_URI specifies scheme ID URI for ID3 timed metadata
diff --git a/src/streaming/controllers/AlternativeMpdController.js b/src/streaming/controllers/AlternativeMpdController.js
new file mode 100644
index 0000000000..c3d0f017fd
--- /dev/null
+++ b/src/streaming/controllers/AlternativeMpdController.js
@@ -0,0 +1,497 @@
+/**
+ * The copyright in this software is being made available under the BSD License,
+ * included below. This software may be subject to other third party and contributor
+ * rights, including patent rights, and no such rights are granted under this license.
+ *
+ * Copyright (c) 2013, Dash Industry Forum.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * * Neither the name of Dash Industry Forum nor the names of its
+ * contributors may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+import Events from '../../core/events/Events.js';
+import MediaPlayerEvents from '../MediaPlayerEvents.js';
+import MediaPlayer from '../MediaPlayer.js';
+import EventBus from './../../core/EventBus.js';
+import FactoryMaker from '../../core/FactoryMaker.js';
+import Constants from '../constants/Constants.js';
+import Settings from '../../core/Settings.js';
+
+
+/*
+TODOS:
+-> Prebuffering could be done in a queue style, where at initialization you preload the first one, when that is done you preload the following, etc
+ -> This is specially good for the timeupdate scheduling approach
+-> I should save the last mainPlayer time reference in order to search current event while in alternative and to easily calculate time back
+-> When going to the alternative you could seek into the content taking into account the duration of the alternative or go back to the time before the alternative
+*/
+
+function AlternativeMpdController() {
+
+ const context = this.context;
+ const eventBus = EventBus(context).getInstance();
+ let settings = Settings(context).getInstance();
+
+ let instance,
+ dashConstants,
+ scheduledEvents = [],
+ eventTimeouts = [],
+ videoModel,
+ bufferedEvent = null,
+ currentEvent = null,
+ isSwitching = false,
+ hideAlternativePlayerControls = false,
+ altPlayer,
+ fullscreenDiv,
+ useDashEventsForScheduling = false,
+ playbackController,
+ altVideoElement,
+ alternativeContext,
+ isMainDynamic = false,
+ lastTimestamp = 0;
+
+ function setConfig(config) {
+ if (!config) {
+ return;
+ }
+
+ if (!videoModel) {
+ videoModel = config.videoModel;
+ }
+ // manifestModel = config.manifestModel;
+ dashConstants = config.DashConstants;
+
+ // if (!altPlayer) {
+ // let mediaPlayerFactory = config.mediaPlayerFactory;
+ // altPlayer = mediaPlayerFactory.create();
+ // }
+
+ if (!!config.playbackController && !playbackController) {
+ playbackController = config.playbackController;
+ }
+
+ if (!!config.hideAlternativePlayerControls && !hideAlternativePlayerControls) {
+ hideAlternativePlayerControls = config.hideAlternativePlayerControls;
+ }
+
+ if (!!config.useDashEventsForScheduling && !useDashEventsForScheduling) {
+ console.log(`Event strategy: ${'Timestamps based' ? config.useDashEventsForScheduling : 'Interval based'}`)
+ useDashEventsForScheduling = config.useDashEventsForScheduling;
+ }
+
+ if (!!config.alternativeContext && !alternativeContext) {
+ alternativeContext = config.alternativeContext
+ }
+
+ if (!!config.lastTimestamp) {
+ lastTimestamp = config.lastTimestamp || 0;
+ }
+
+ if (!!config.currentEvent && !currentEvent) {
+ currentEvent = config.currentEvent;
+ }
+ }
+
+ function initialize() {
+ eventBus.on(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this);
+ if (altPlayer) {
+ altPlayer.on(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this);
+ }
+
+ document.addEventListener('fullscreenchange', () => {
+ if (document.fullscreenElement === videoModel.getElement()) {
+ // document.exitFullscreen();
+ // fullscreenDiv.requestFullscreen();
+ } else {
+ // document.exitFullscreen();
+ }
+ });
+
+ // Borrar por el amor de dios
+ if (!fullscreenDiv) {
+ fullscreenDiv = document.createElement('div');
+ fullscreenDiv.id = 'fullscreenDiv';
+ videoModel.getElement().parentNode.insertBefore(fullscreenDiv, videoModel.getElement());
+ fullscreenDiv.appendChild(videoModel.getElement());
+ }
+
+ eventBus.on(Constants.ALTERNATIVE_MPD.URI, _onAlternativeLoad);
+ }
+
+ function _onAlternativeLoad(e) {
+ console.log(e);
+ // console.log('I\'m coming from an alternative based event');
+ }
+
+ function _onManifestLoaded(e) {
+ const manifest = e.data;
+ const events = _parseAlternativeMPDEvents(manifest)
+ if (scheduledEvents && scheduledEvents.length > 0) {
+ scheduledEvents.push(...events)
+ } else {
+ scheduledEvents = events
+ }
+
+ scheduledEvents.forEach((d) => { if (d.alternativeMPD.uri == e.data.originalUrl) { d.type = e.data.type } });
+
+ switch (manifest.type) {
+ case 'dynamic':
+ if (!currentEvent && !altPlayer) {
+ isMainDynamic = true;
+ _scheduleAlternativeMPDEvents();
+ }
+ break;
+ case 'static':
+ if (!isMainDynamic) {
+ _prebufferNextAlternative();
+ _startDashEventPlaybackTimeMonitoring();
+ }
+ break;
+ default:
+ console.log('Unknown manifest type')
+ break;
+ }
+ }
+
+ function _startDashEventPlaybackTimeMonitoring() {
+ eventBus.on(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onDashPlaybackTimeUpdated, this);
+ if (altPlayer) {
+ altPlayer.on(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onDashPlaybackTimeUpdated, this);
+ }
+ }
+
+ function _onDashPlaybackTimeUpdated(e) {
+ try {
+ if (currentEvent) {
+ if (currentEvent.type == 'dynamic') { return; }
+ if (currentEvent.duration <= e.time && Math.round(e.time - lastTimestamp) != 0) {
+ _switchBackToMainContent(currentEvent);
+ }
+ }
+ else {
+ const currentTime = e.time;
+ lastTimestamp = e.time;
+ const event = _getCurrentEvent(currentTime);
+
+ if (event && !isSwitching && !currentEvent) {
+ currentEvent = event;
+ _switchToAlternativeContent(event);
+ }
+ }
+ } catch (err) {
+ // console.log(lastTimestamp);
+ console.error('Error in onDashPlaybackTimeUpdated:', err);
+ }
+ }
+
+ function _getCurrentEvent(currentTime) {
+ return scheduledEvents.find(event => {
+ if (event.watched && event.mode === 'insert') {
+ return false;
+ }
+ return currentTime >= event.presentationTime &&
+ currentTime < event.presentationTime + event.duration - event.returnOffset;
+ });
+ }
+
+ function _initializeAlternativePlayerElement(event) {
+ if (!altVideoElement) {
+ // Create a new video element for the alternative content
+ altVideoElement = document.createElement('video');
+ altVideoElement.style.display = 'none'; // Hide the alternative video element initially
+ altVideoElement.autoplay = false;
+ altVideoElement.controls = !hideAlternativePlayerControls;
+ fullscreenDiv.appendChild(altVideoElement);
+
+ // Insert the alternative video element into the DOM
+ videoModel.getElement().parentNode.insertBefore(altVideoElement, videoModel.getElement().nextSibling);
+ };
+
+ // Initialize alternative player
+ if (event != bufferedEvent) {
+ _initializeAlternativePlayer(event);
+ }
+ }
+
+ function _initializeAlternativePlayer(event) {
+ // Initialize alternative player
+ altPlayer = MediaPlayer().create();
+
+ // Apply same CMCD settings as Main Player, set int as true and a diffrent cid.
+ const cmcdSettings = {...settings.get().streaming.cmcd};
+ const cmcdSubIncdex = event.id ? event.id : 'ad';
+ cmcdSettings.cid = `${cmcdSettings.cid}#${cmcdSubIncdex}`;
+ cmcdSettings.int = true;
+ altPlayer.updateSettings({streaming: {cmcd: cmcdSettings}});
+
+ // TODO: Remove hack: I can not disable CMCD in any starnard way, so I created this custom attribute to have contrl with CMCD Request Mode
+ console.log(event.originalEvent)
+ if (event.originalEvent.AlternativeMPD['cmcd:RequestModeEnabled'] == 'false') {
+ cmcdSettings.reporting.requestMode.enabled = false
+ }
+ altPlayer.updateSettings({streaming: {cmcd: cmcdSettings}});
+
+ altPlayer.initialize(altVideoElement, event.alternativeMPD.uri, false, NaN, alternativeContext);
+ altPlayer.setAutoPlay(false);
+
+ altPlayer.on(Events.ERROR, (e) => {
+ console.error('Alternative player error:', e);
+ }, this);
+ }
+
+ function _parseAlternativeMPDEvents(manifest) {
+ const events = [];
+ const periods = manifest.Period || [];
+
+ periods.forEach(period => {
+ const eventStreams = period.EventStream || [];
+ eventStreams.forEach(eventStream => {
+ if (eventStream.schemeIdUri === dashConstants.ALTERNATIVE_MPD_SCHEME_ID) {
+ const timescale = eventStream.timescale || 1;
+ const eventsArray = eventStream.Event || [];
+ eventsArray.forEach(ev => {
+ if (ev && ev.AlternativeMPD) {
+ const alternativeMPDNode = ev.AlternativeMPD;
+ const mode = alternativeMPDNode.mode || 'insert';
+ const eventObj = { //This should be casted using an AlternativeMpdObject
+ presentationTime: ev.presentationTime / timescale,
+ duration: ev.duration / timescale,
+ alternativeMPD: {
+ uri: alternativeMPDNode.uri,
+ earliestResolutionTimeOffset: parseInt(alternativeMPDNode.earliestResolutionTimeOffset || '0', 10) / 1000,
+ },
+ mode: mode,
+ returnOffset: parseInt(alternativeMPDNode.returnOffset || '0', 10) / 1000,
+ triggered: false,
+ watched: false,
+ type: 'static',
+ id: ev.id,
+ originalEvent: ev
+
+ };
+ events.push(eventObj);
+ }
+ });
+ }
+ });
+ });
+
+ return events;
+ }
+
+ function _scheduleAlternativeMPDEvents() {
+ scheduledEvents.forEach(event => {
+ if (event.length == 0) {
+ return;
+ }
+ const currentTime = videoModel.getElement().currentTime;
+ const timeToEvent = event.presentationTime - currentTime;
+ const timeToPrebuffer = (event.presentationTime - event.alternativeMPD.earliestResolutionTimeOffset) - currentTime;
+
+ if (timeToPrebuffer >= 0) {
+ const prebufferTimeoutId = setTimeout(() => {
+ _prebufferAlternativeContent(event);
+ }, timeToPrebuffer * 1000);
+
+ eventTimeouts.push(prebufferTimeoutId);
+ } else {
+ _prebufferAlternativeContent(event);
+ }
+
+ if (timeToEvent >= 0) {
+ const switchToAltTimeoutId = setTimeout(() => {
+ _switchToAlternativeContent(event);
+ }, timeToEvent * 1000);
+
+ eventTimeouts.push(switchToAltTimeoutId);
+
+ const switchToMainTimeoutId = setTimeout(() => {
+ _switchBackToMainContent(event);
+ }, (timeToEvent + event.duration) * 1000);
+
+ eventTimeouts.push(switchToMainTimeoutId);
+ } else if (currentTime < event.presentationTime + event.duration) {
+ _switchToAlternativeContent(event);
+
+ const timeLeft = (event.presentationTime + event.duration - currentTime) * 1000;
+ const switchToMainTimeoutId = setTimeout(() => {
+ _switchBackToMainContent(event);
+ }, timeLeft);
+
+ eventTimeouts.push(switchToMainTimeoutId);
+ }
+ });
+ }
+
+ function _descheduleAlternativeMPDEvents(event) {
+ if (currentEvent) {
+ setTimeout(() => {
+ _switchBackToMainContent(event);
+ }, event.duration * 1000);
+ // currentEvent = null;
+ }
+ }
+
+ function _prebufferNextAlternative() {
+ const nextEvent = scheduledEvents.find(event => {
+ if (event.watched && event.mode === 'insert') {
+ return false;
+ }
+ return !event.triggered;
+ });
+
+ if (nextEvent && !bufferedEvent) {
+ console.log(`Preloading event starting at ${nextEvent.presentationTime}`)
+ _prebufferAlternativeContent(nextEvent);
+ }
+ }
+
+ function _prebufferAlternativeContent(event) {
+ if (event.triggered) { return };
+ const idx = scheduledEvents.findIndex(e => e == event);
+ if (idx !== -1) {
+ scheduledEvents[idx].triggered = true;
+ }
+
+ _initializeAlternativePlayerElement(event);
+ bufferedEvent = event;
+
+ altPlayer.on(Events.STREAM_INITIALIZED, () => {
+ // console.log('I\'m buffering')
+ // altPlayer.seek(event.earliestResolutionTimeOffset);
+ // Do not play yet, just buffer
+ }, this);
+ }
+
+ function _switchToAlternativeContent(event) {
+ if (isSwitching) { return };
+ isSwitching = true;
+ const idx = scheduledEvents.findIndex(e => e === event);
+ if (idx !== -1) {
+ scheduledEvents[idx].triggered = true;
+ }
+
+ _initializeAlternativePlayerElement(event);
+
+ if (event.type == 'dynamic') {
+ _descheduleAlternativeMPDEvents(currentEvent);
+ }
+
+ videoModel.pause();
+
+ videoModel.getElement().style.display = 'none';
+ altVideoElement.style.display = 'block';
+
+ altPlayer.play();
+
+ // Sends the player and the event that trigges this alternative content.
+ eventBus.trigger(MediaPlayerEvents.ALTERNATIVE_PLAYBACK_PLAYING, { sender: this, altPlayer, event});
+
+ isSwitching = false;
+ bufferedEvent = null;
+ }
+
+ function _switchBackToMainContent(event) {
+ if (isSwitching) { return };
+ isSwitching = true;
+
+ altPlayer.pause();
+
+ // Sends the player and the event that trigges this alternative content.
+ eventBus.trigger(MediaPlayerEvents.ALTERNATIVE_PLAYBACK_ENDED, { sender: this, altPlayer, event});
+
+ altVideoElement.style.display = 'none';
+ videoModel.getElement().style.display = 'block';
+
+ let seekTime;
+ if (event.mode === 'replace') {
+ seekTime = event.presentationTime + event.duration - event.returnOffset;
+ } else if (event.mode === 'insert') {
+ if (!event.watched) {
+ const idx = scheduledEvents.findIndex(e => e === event);
+ if (idx !== -1) {
+ scheduledEvents[idx].watched = true;
+ }
+ }
+ seekTime = event.presentationTime;
+ }
+
+ if (playbackController.getIsDynamic()) {
+ playbackController.seekToOriginalLive(true, false, false);
+ } else {
+ playbackController.seek(seekTime, false, false);
+ }
+
+ videoModel.play();
+
+ altPlayer.reset();
+ altPlayer = null;
+
+ altVideoElement.parentNode.removeChild(altVideoElement);
+ altVideoElement = null;
+
+ isSwitching = false;
+ currentEvent = null;
+
+ _prebufferNextAlternative();
+ }
+
+
+ function reset() {
+ scheduledEvents = [];
+ eventTimeouts.forEach(timeoutId => clearTimeout(timeoutId));
+ eventTimeouts = [];
+
+ if (altPlayer) {
+ altPlayer.off(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this);
+ altPlayer.reset();
+ altPlayer = null;
+ }
+
+ if (altVideoElement) {
+ altVideoElement.parentNode.removeChild(altVideoElement);
+ altVideoElement = null;
+ }
+
+ if (useDashEventsForScheduling && videoModel) {
+ videoModel.off(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _onDashPlaybackTimeUpdated, this);
+ }
+
+ isSwitching = false;
+ currentEvent = null;
+
+ eventBus.off(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this);
+ }
+
+ instance = {
+ setConfig,
+ initialize,
+ reset
+ };
+
+ return instance;
+}
+
+AlternativeMpdController.__dashjs_factory_name = 'AlternativeMpdController';
+const factory = FactoryMaker.getSingletonFactory(AlternativeMpdController);
+FactoryMaker.updateSingletonFactory(AlternativeMpdController.__dashjs_factory_name, factory);
+export default factory;
diff --git a/src/streaming/controllers/EventController.js b/src/streaming/controllers/EventController.js
index 837bcabf2e..e86d5ca149 100644
--- a/src/streaming/controllers/EventController.js
+++ b/src/streaming/controllers/EventController.js
@@ -474,6 +474,7 @@ function EventController() {
if (mode === MediaPlayerEvents.EVENT_MODE_ON_RECEIVE && !event.triggeredReceivedEvent) {
logger.debug(`Received event ${eventId}`);
event.triggeredReceivedEvent = true;
+ console.log(`Triggered: ${event.eventStream.schemeIdUri}`);
eventBus.trigger(event.eventStream.schemeIdUri, { event: event }, { mode });
return;
}
diff --git a/src/streaming/controllers/ExtUrlQueryInfoController.js b/src/streaming/controllers/ExtUrlQueryInfoController.js
index 1a026c81d1..48da921ab7 100644
--- a/src/streaming/controllers/ExtUrlQueryInfoController.js
+++ b/src/streaming/controllers/ExtUrlQueryInfoController.js
@@ -133,20 +133,22 @@ function ExtUrlQueryInfoController() {
};
_generateQueryParams(periodObject, period, mpdUrlQuery, mpdQueryStringInformation, DashConstants.PERIOD);
- period.AdaptationSet.forEach((adaptationSet) => {
- const adaptationObject = {
- representation: []
- };
- _generateQueryParams(adaptationObject, adaptationSet, mpdUrlQuery, periodObject, DashConstants.ADAPTATION_SET);
-
- adaptationSet.Representation.forEach((representation) => {
- const representationObject = {};
- _generateQueryParams(representationObject, representation, mpdUrlQuery, adaptationObject, DashConstants.REPRESENTATION);
-
- adaptationObject.representation.push(representationObject);
+ if (!period.ImportedMPD) {
+ period.AdaptationSet.forEach((adaptationSet) => {
+ const adaptationObject = {
+ representation: []
+ };
+ _generateQueryParams(adaptationObject, adaptationSet, mpdUrlQuery, periodObject, DashConstants.ADAPTATION_SET);
+
+ adaptationSet.Representation.forEach((representation) => {
+ const representationObject = {};
+ _generateQueryParams(representationObject, representation, mpdUrlQuery, adaptationObject, DashConstants.REPRESENTATION);
+
+ adaptationObject.representation.push(representationObject);
+ });
+ periodObject.adaptation.push(adaptationObject);
});
- periodObject.adaptation.push(adaptationObject);
- });
+ }
mpdQueryStringInformation.period.push(periodObject);
});
}
diff --git a/src/streaming/controllers/ListMpdController.js b/src/streaming/controllers/ListMpdController.js
new file mode 100644
index 0000000000..ccfb202e88
--- /dev/null
+++ b/src/streaming/controllers/ListMpdController.js
@@ -0,0 +1,170 @@
+/**
+ * The copyright in this software is being made available under the BSD License,
+ * included below. This software may be subject to other third party and contributor
+ * rights, including patent rights, and no such rights are granted under this license.
+ *
+ * Copyright (c) 2013, Dash Industry Forum.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * * Neither the name of Dash Industry Forum nor the names of its
+ * contributors may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+// import XlinkLoader from '../XlinkLoader.js';
+import EventBus from '../../core/EventBus.js';
+import Events from '../../core/events/Events.js';
+import MediaPlayerEvents from '../MediaPlayerEvents.js';
+import FactoryMaker from '../../core/FactoryMaker.js';
+import ManifestLoader from '../ManifestLoader.js';
+import DashConstants from '../../dash/constants/DashConstants.js';
+
+
+function ListMpdController() {
+
+ let context = this.context;
+ let eventBus = EventBus(context).getInstance();
+
+ let instance,
+ settings,
+ linkedPeriodList,
+ dashAdapter,
+ currentManifest,
+ manifestLoader,
+ mpdHasDuration
+
+ function setConfig(config) {
+ if (!config) {
+ return;
+ }
+
+ if (config.settings) {
+ settings = config.settings;
+ }
+
+ if (config.dashAdapter) {
+ dashAdapter = config.dashAdapter;
+ }
+ }
+
+ function initialize() {
+ eventBus.on(Events.IMPORTED_MPDS_LOADED, _onLinkedPeriodsLoaded, instance);
+ eventBus.on(MediaPlayerEvents.PLAYBACK_TIME_UPDATED, _triggerLoadImportMpd, instance);
+ manifestLoader = ManifestLoader(context).getInstance();
+ }
+
+ function loadListMpdManifest(time) {
+ linkedPeriodList.forEach(linkedPeriod => {
+ if (_shouldLoadLinkedPeriod(linkedPeriod, time)) {
+ loadLinkedPeriod(currentManifest, linkedPeriod);
+ }
+ });
+ }
+
+ function _onLinkedPeriodsLoaded({ manifest, linkedPeriods }) {
+ currentManifest = manifest;
+ linkedPeriodList = linkedPeriods;
+
+ if (manifest.Period[0].start) {
+ throw new Error('The first period in a list MPD must have start time equal to 0');
+ }
+
+ manifest.Period[0].start = 0;
+
+ mpdHasDuration = manifest.hasOwnProperty(DashConstants.MEDIA_PRESENTATION_DURATION);
+ if (!mpdHasDuration) {
+ manifest.mediaPresentationDuration = 0;
+ for (let i = manifest.Period.length - 1; i >= 0; i--) {
+ manifest.mediaPresentationDuration += manifest.Period[i].duration;
+ if (manifest.Period[i].start) {
+ manifest.mediaPresentationDuration += manifest.Period[i].start;
+ break;
+ }
+ }
+ }
+
+ const startPeriod = linkedPeriodList.find(period => period.start === 0);
+ if (startPeriod) {
+ loadLinkedPeriod(manifest, startPeriod);
+ } else {
+ eventBus.trigger(Events.MANIFEST_UPDATED, { manifest: manifest });
+ }
+ }
+
+ function loadLinkedPeriod(manifest, period) {
+ let baseUri = period.ImportedMPD.uri;
+ if (manifest.baseURL) {
+ baseUri = manifest.BaseURL[0].__text + period.ImportedMPD.uri;
+ }
+ const updatedManifest = new Promise(resolve => {
+ manifestLoader.load(baseUri, null, null, true)
+ .then((importedManifest) => {
+ dashAdapter.mergeManifests(manifest, importedManifest, period.id, mpdHasDuration);
+ }, () => {
+ dashAdapter.mergeManifests(manifest, null, period.id, mpdHasDuration);
+ })
+ .then(() => {
+ eventBus.trigger(Events.MANIFEST_UPDATED, { manifest });
+ linkedPeriodList = linkedPeriodList.filter((element) => element.id !== period.id);
+ resolve(manifest);
+ });
+ });
+ return updatedManifest;
+ }
+
+ function _triggerLoadImportMpd(e) {
+ if (!linkedPeriodList || linkedPeriodList.lenght) {
+ return;
+ }
+
+ loadListMpdManifest(e.time);
+ }
+
+ function _shouldLoadLinkedPeriod(linkedPeriod, time) {
+ if (!linkedPeriod.ImportedMPD) {
+ return false
+ }
+
+ const { minEarliestResolutionTimeOffset } = settings.get().streaming.listMpd;
+ const { earliestResolutionTimeOffset } = linkedPeriod.ImportedMPD;
+ const resolutionTime = Math.max(earliestResolutionTimeOffset, minEarliestResolutionTimeOffset);
+
+ return time >= linkedPeriod.start - resolutionTime;
+ }
+
+ function reset() {
+ linkedPeriodList = [];
+ }
+
+ instance = {
+ initialize,
+ loadListMpdManifest,
+ loadLinkedPeriod,
+ reset,
+ setConfig
+ };
+
+ return instance;
+}
+
+ListMpdController.__dashjs_factory_name = 'ListMpdController';
+const factory = FactoryMaker.getSingletonFactory(ListMpdController);
+FactoryMaker.updateSingletonFactory(ListMpdController.__dashjs_factory_name, factory);
+export default factory;
diff --git a/src/streaming/controllers/PlaybackController.js b/src/streaming/controllers/PlaybackController.js
index cc5503aa77..6e1bf5ce7f 100644
--- a/src/streaming/controllers/PlaybackController.js
+++ b/src/streaming/controllers/PlaybackController.js
@@ -703,7 +703,9 @@ function PlaybackController() {
}
function _onPlaybackProgress() {
- eventBus.trigger(Events.PLAYBACK_PROGRESS, { streamId: streamInfo.id });
+ if (streamInfo){
+ eventBus.trigger(Events.PLAYBACK_PROGRESS, { streamId: streamInfo.id });
+ }
}
function _onPlaybackRateChanged() {
diff --git a/src/streaming/controllers/StreamController.js b/src/streaming/controllers/StreamController.js
index 9cf3604271..3f889a9c4d 100644
--- a/src/streaming/controllers/StreamController.js
+++ b/src/streaming/controllers/StreamController.js
@@ -48,6 +48,7 @@ import ConformanceViolationConstants from '../constants/ConformanceViolationCons
import ExtUrlQueryInfoController from './ExtUrlQueryInfoController.js';
import ProtectionEvents from '../protection/ProtectionEvents.js';
import ProtectionErrors from '../protection/errors/ProtectionErrors.js';
+import ListMpdController from './ListMpdController.js';
const PLAYBACK_ENDED_TIMER_INTERVAL = 200;
const DVR_WAITING_OFFSET = 2;
@@ -61,7 +62,7 @@ function StreamController() {
dashMetrics, mediaSourceController, timeSyncController, contentSteeringController, baseURLController,
segmentBaseController, uriFragmentModel, abrController, throughputController, mediaController, eventController,
initCache, errHandler, timelineConverter, streams, activeStream, protectionController, textController,
- protectionData, extUrlQueryInfoController,
+ protectionData, extUrlQueryInfoController, listMpdController,
autoPlay, isStreamSwitchingInProgress, hasMediaError, hasInitialisationError, mediaSource, videoModel,
playbackController, serviceDescriptionController, mediaPlayerModel, customParametersModel, isPaused,
initialPlayback, initialSteeringRequest, playbackEndedTimerInterval, bufferSinks, preloadingStreams, settings,
@@ -101,6 +102,7 @@ function StreamController() {
eventController.start();
extUrlQueryInfoController = ExtUrlQueryInfoController(context).getInstance();
+ listMpdController = ListMpdController(context).getInstance();
timeSyncController.setConfig({
dashMetrics, baseURLController, errHandler, settings
@@ -630,11 +632,21 @@ function StreamController() {
});
Promise.all(promises)
+ .then(() => {
+ const seekToPeriod = manifestModel.getValue().Period[seekToStream.getId()];
+ if (seekToPeriod.ImportedMPD) {
+ return listMpdController
+ .loadLinkedPeriod(manifestModel.getValue(), seekToPeriod)
+ .then(updatedManifest => {
+ baseURLController.update(updatedManifest);
+ });
+ }
+ })
.then(() => {
_switchStream(seekToStream, activeStream, seekTime);
})
- .catch((e) => {
- errHandler.error(e);
+ .catch(error => {
+ console.error(error);
});
}
@@ -652,6 +664,8 @@ function StreamController() {
// If the track was changed in the active stream we need to stop preloading and remove the already prebuffered stuff. Since we do not support preloading specific handling of specific AdaptationSets yet.
_deactivateAllPreloadingStreams();
+ console.log('_onCurrentTrackChanged');
+
if (settings.get().streaming.buffer.resetSourceBuffersForTrackSwitch && e.oldMediaInfo && e.oldMediaInfo.codec !== e.newMediaInfo.codec) {
const seekTime = playbackController.getTime();
activeStream.deactivate(false);
@@ -892,7 +906,8 @@ function StreamController() {
const previousStream = i === 0 ? activeStream : upcomingStreams[i - 1];
// If the preloading for the current stream is not scheduled, but its predecessor has finished buffering we can start prebuffering this stream
- if (!stream.getPreloaded() && previousStream.getHasFinishedBuffering()) {
+ const isLinkedPeriod = manifestModel.getValue().Period[stream.getId()].ImportedMPD;
+ if (!stream.getPreloaded() && previousStream.getHasFinishedBuffering() && !isLinkedPeriod) {
if (mediaSource) {
_onStreamCanLoadNext(stream, previousStream);
}
@@ -979,8 +994,13 @@ function StreamController() {
activeStream.setIsEndedEventSignaled(true);
const nextStream = _getNextStream();
if (nextStream) {
- logger.debug(`StreamController onEnded, found next stream with id ${nextStream.getStreamInfo().id}. Switching from ${activeStream.getStreamInfo().id} to ${nextStream.getStreamInfo().id}`);
- _switchStream(nextStream, activeStream, NaN);
+ const streamId = nextStream.getStreamInfo().id;
+ logger.debug(`StreamController onEnded, found next stream with id ${streamId}. Switching from ${activeStream.getStreamInfo().id} to ${nextStream.getStreamInfo().id}`);
+ const nextPeriod = manifestModel.getValue().Period[streamId];
+ console.log(nextPeriod)
+ if (!nextPeriod.ImportedMPD) {
+ _switchStream(nextStream, activeStream, NaN);
+ }
} else {
logger.debug('StreamController no next stream found');
activeStream.setIsEndedEventSignaled(false);
@@ -1313,6 +1333,8 @@ function StreamController() {
function switchToVideoElement(seekTime) {
+ console.log('switchToVideoElement');
+
if (activeStream) {
playbackController.initialize(getActiveStreamInfo());
_openMediaSource({ seekTime, keepBuffers: false, streamActivated: true });
@@ -1506,6 +1528,9 @@ function StreamController() {
return;
}
+ if (config.streams) {
+ streams = config.streams;
+ }
if (config.capabilities) {
capabilities = config.capabilities;
}
@@ -1610,6 +1635,50 @@ function StreamController() {
}
}
+ function resetAlt() {
+ // _checkConfig();
+
+ // timeSyncController.reset();
+
+ // _flushPlaylistMetrics(hasMediaError || hasInitialisationError ? PlayListTrace.FAILURE_STOP_REASON : PlayListTrace.USER_REQUEST_STOP_REASON);
+ const config = {};
+ config.streamsCpy = [...streams];
+
+ for (let i = 0, ln = streams ? streams.length : 0; i < ln; i++) {
+ const stream = streams[i];
+ stream.resetAlt(true);
+ }
+
+ unRegisterEvents();
+
+ // baseURLController.reset();
+ // manifestUpdater.reset();
+ // eventController.reset();
+ // dashMetrics.clearAllCurrentMetrics();
+ // manifestModel.setValue(null);
+ // manifestLoader.reset();
+ // timelineConverter.reset();
+ // initCache.reset();
+
+ // if (mediaSource) {
+ // mediaSourceController.detachMediaSource(videoModel);
+ // mediaSource = null;
+ // }
+ // videoModel = null;
+ // if (protectionController) {
+ // protectionController = null;
+ // protectionData = null;
+ // if (manifestModel.getValue()) {
+ // eventBus.trigger(Events.PROTECTION_DESTROYED, { data: manifestModel.getValue().url });
+ // }
+ // }
+
+ _stopPlaybackEndedTimerInterval();
+ eventBus.trigger(Events.STREAM_TEARDOWN_COMPLETE);
+ resetInitialSettings();
+ return config;
+ }
+
function reset() {
_checkConfig();
@@ -1692,6 +1761,7 @@ function StreamController() {
loadWithManifest,
refreshManifest,
reset,
+ resetAlt,
setConfig,
setProtectionData,
switchToVideoElement,
diff --git a/src/streaming/models/CmcdModel.js b/src/streaming/models/CmcdModel.js
index c689097671..4143921ecf 100644
--- a/src/streaming/models/CmcdModel.js
+++ b/src/streaming/models/CmcdModel.js
@@ -61,10 +61,13 @@ function CmcdModel() {
serviceDescriptionController,
throughputController,
streamProcessors,
+ _eventTimeoutId,
+ _msdSent,
_lastMediaTypeRequest,
_isStartup,
_bufferLevelStarved,
- _initialMediaRequestsDone;
+ _initialMediaRequestsDone,
+ _playbackStartedTime;
let context = this.context;
let eventBus = EventBus(context).getInstance();
@@ -83,6 +86,25 @@ function CmcdModel() {
eventBus.on(MediaPlayerEvents.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, instance);
eventBus.on(MediaPlayerEvents.PLAYBACK_SEEKED, _onPlaybackSeeked, instance);
eventBus.on(MediaPlayerEvents.PERIOD_SWITCH_COMPLETED, _onPeriodSwitchComplete, instance);
+ eventBus.on(MediaPlayerEvents.PLAYBACK_INITIALIZED, _onPlaybackInitialized, instance);
+ eventBus.on(MediaPlayerEvents.PLAYBACK_STARTED, _onPlaybackStarted, instance);
+ eventBus.on(MediaPlayerEvents.PLAYBACK_PAUSED, _onPlaybackPaused, instance);
+ eventBus.on(MediaPlayerEvents.PLAYBACK_PLAYING, _onPlaybackPlaying, instance)
+ eventBus.on(MediaPlayerEvents.PLAYBACK_SEEKING, _onPlaybackSeeking, instance);
+ eventBus.on(MediaPlayerEvents.PLAYBACK_STALLED, _onPlaybackStalled, instance);
+ eventBus.on(MediaPlayerEvents.PLAYBACK_ERROR, _onPlaybackError, instance);
+ eventBus.on(MediaPlayerEvents.ERROR, _onPlayerError, instance);
+ eventBus.on(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, instance);
+ eventBus.on(MediaPlayerEvents.ALTERNATIVE_PLAYBACK_PLAYING, _onAlternativeStarted, instance);
+ eventBus.on(MediaPlayerEvents.ALTERNATIVE_PLAYBACK_ENDED, _onAlternativeEnded, instance);
+
+ const cmcdEventMode = _getCmcdEventData();
+ if (cmcdEventMode){
+ const interval = settings.get().streaming.cmcd.reporting.eventMode.interval;
+ if (interval !== 0) {
+ _startCmcdEventTimer(interval, cmcdEventMode);
+ }
+ }
}
function setConfig(config) {
@@ -118,19 +140,151 @@ function CmcdModel() {
st: null,
sf: null,
sid: `${Utils.generateUuid()}`,
- cid: null
+ cid: null,
+ sta: null,
+ bg: null
};
+ _eventTimeoutId = null;
+ _msdSent = [false, false, false];
_bufferLevelStarved = {};
_isStartup = {};
_initialMediaRequestsDone = {};
_lastMediaTypeRequest = undefined;
_updateStreamProcessors();
+ _setCmcdVersion();
}
function _onPeriodSwitchComplete() {
_updateStreamProcessors();
}
+ function _onPlaybackInitialized() {
+ _onStateChange('s');
+ }
+
+ function _onPlaybackStarted() {
+ _onStateChange('p');
+ }
+
+ function _onPlaybackPaused() {
+ _onStateChange('a');
+ }
+
+ function _onPlaybackPlaying() {
+ _onStateChange('p');
+ }
+
+ function _onPlaybackSeeking() {
+ _onStateChange('k');
+ }
+
+ function _onPlaybackStalled() {
+ _onStateChange('r');
+ }
+
+ function _onPlaybackError() {
+ _onStateChange('f');
+ }
+
+ function _onPlaybackEnded() {
+ _onStateChange('e');
+ }
+
+ function _onStateChange(state) {
+ if (!internalData.msd){
+ // Calculate msd key
+ if (!_playbackStartedTime && state === 'p'){
+ _playbackStartedTime = Date.now();
+ }
+ if (_playbackStartedTime && state === 'pl'){
+ internalData.msd = Date.now() - _playbackStartedTime
+ return
+ }
+ }
+
+ if (internalData.state !== state) {
+ const cmcdEventMode = _getCmcdEventData();
+ internalData.sta = state;
+ if (cmcdEventMode){
+ _sendCmcdEventData(cmcdEventMode, 'ps');
+ }
+ }
+ }
+
+ function _onPlayerError(data){
+ console.log('ERROR: ', data)
+ _sendCmcdEventData(_getCmcdEventData(),'e')
+
+ }
+
+ function _onAlternativeStarted (data) {
+ _updateStreamProcessors()
+ console.log('CMCD: Alternative content started', data)
+ // When interstitial starts, the player goes in background.
+ internalData.bg = true
+ _sendCmcdEventData(_getCmcdEventData(),'i')
+ // This player is not changing content.
+ //_sendCmcdEventData(_getCmcdEventData(),'c')
+ }
+
+ function _onAlternativeEnded (data) {
+ _updateStreamProcessors()
+ console.log('CMCD: Alternative content ended', data)
+ internalData.bg = null
+ _sendCmcdEventData(_getCmcdEventData(),'i')
+ // This player is not changing content.
+ //_sendCmcdEventData(_getCmcdEventData(),'c')
+ }
+
+ function _sendCmcdEventData(cmcdEventMode, eventKeyValue = null) {
+ const cmcdData = _getGenericCmcdData(null);
+
+ // Add the event key data.
+ cmcdData.e = eventKeyValue
+ const filteredCmcdData = _applyWhitelist(cmcdData, 3);
+
+ var requestUrl = cmcdEventMode.requestUrl;
+ var headers = {}
+
+ if (cmcdEventMode.mode === Constants.CMCD_MODE_QUERY) {
+ const additionalQueryParameter = [];
+ const cmcdQueryParams = encodeCmcd(filteredCmcdData);
+ if (cmcdQueryParams) {
+ additionalQueryParameter.push({key: CMCD_PARAM, value: cmcdQueryParams});
+ }
+ requestUrl = Utils.addAdditionalQueryParameterToUrl(requestUrl, additionalQueryParameter);
+ } else if (cmcdEventMode.mode === Constants.CMCD_MODE_HEADER) {
+ headers = toCmcdHeaders(filteredCmcdData)
+ }
+
+ fetch(requestUrl, {
+ method: cmcdEventMode.requestMethod,
+ headers: headers
+ }).then(response => {
+ console.log('Event CMCD data sent successfully:', response);
+ }).catch(error => {
+ console.error('Error sending event CMCD data:', error);
+ });
+ }
+
+ function _startCmcdEventTimer(interval, eventMode) {
+ _eventTimeoutId = setTimeout(() => {
+ _sendCmcdEventData(eventMode, 't')
+ // Restart the timer
+ _startCmcdEventTimer(interval, eventMode);
+ }, interval);
+ }
+
+ function _getCmcdEventData() {
+ if (isCmcdEnabled() && internalData.v === 2) {
+ const cmcdEventMode = settings.get().streaming.cmcd.reporting.eventMode;
+ if (cmcdEventMode && cmcdEventMode.enabled) {
+ return cmcdEventMode
+ }
+ }
+ return null
+ }
+
function _updateStreamProcessors() {
if (!playbackController) {
return;
@@ -149,19 +303,21 @@ function CmcdModel() {
streamProcessors = activeStream.getStreamProcessors();
}
- function getQueryParameter(request) {
+ function getQueryParameter(request, triggerEvent = true, cmcdReportingMode = null) {
try {
if (isCmcdEnabled()) {
const cmcdData = getCmcdData(request);
- const filteredCmcdData = _applyWhitelist(cmcdData);
+ const filteredCmcdData = _applyWhitelist(cmcdData, cmcdReportingMode);
const finalPayloadString = encodeCmcd(filteredCmcdData);
- eventBus.trigger(MetricsReportingEvents.CMCD_DATA_GENERATED, {
- url: request.url,
- mediaType: request.mediaType,
- cmcdData,
- cmcdString: finalPayloadString
- });
+ if (triggerEvent){
+ eventBus.trigger(MetricsReportingEvents.CMCD_DATA_GENERATED, {
+ url: request.url,
+ mediaType: request.mediaType,
+ cmcdData,
+ cmcdString: finalPayloadString
+ });
+ }
return {
key: CMCD_PARAM,
value: finalPayloadString
@@ -173,12 +329,48 @@ function CmcdModel() {
return null;
}
}
-
- function _applyWhitelist(cmcdData) {
+
+ function _applyWhitelist(cmcdData, cmcdReportingMode) {
try {
const cmcdParametersFromManifest = getCmcdParametersFromManifest();
- const enabledCMCDKeys = cmcdParametersFromManifest.version ? cmcdParametersFromManifest.keys : settings.get().streaming.cmcd.enabledKeys;
-
+ var enabledCMCDKeys = cmcdParametersFromManifest.version ? cmcdParametersFromManifest.keys : settings.get().streaming.cmcd.enabledKeys;
+
+ // MSD key must be send once per mode.
+ if (cmcdData.msd) {
+ const msdSentIndex = cmcdReportingMode - 1;
+ if (_msdSent[msdSentIndex]) {
+ delete cmcdData.msd;
+ } else {
+ _msdSent[msdSentIndex] = true;
+ }
+ }
+ // For CMCD v2 use the reporting mode keys or global ones as default
+ // TODO: Try to improve this block by using _checkAvailableKeys(cmcdParametersFromManifest)
+ if (cmcdReportingMode === 1){
+ enabledCMCDKeys = settings.get().streaming.cmcd.reporting.requestMode.enabledKeys ? settings.get().streaming.cmcd.reporting.requestMode.enabledKeys : settings.get().streaming.cmcd.enabledKeys;
+ // Remove unsupported keys
+ enabledCMCDKeys = enabledCMCDKeys.filter(item => item !== 'url');
+ } else if (cmcdReportingMode === 2) {
+ enabledCMCDKeys = settings.get().streaming.cmcd.reporting.responseMode.enabledKeys ? settings.get().streaming.cmcd.reporting.responseMode.enabledKeys : settings.get().streaming.cmcd.enabledKeys;
+ // Add CMCD v2 response mode mandatory keys
+ const requiredKeys = ['ts', 'url'];
+ requiredKeys.forEach(key => {
+ if (!enabledCMCDKeys.includes(key)) {
+ enabledCMCDKeys.push(key);
+ }
+ });
+ } else if (cmcdReportingMode === 3) {
+ enabledCMCDKeys = settings.get().streaming.cmcd.reporting.eventMode.enabledKeys ? settings.get().streaming.cmcd.reporting.eventMode.enabledKeys : settings.get().streaming.cmcd.enabledKeys;
+ // Remove unsupported Event mode keys
+ enabledCMCDKeys = enabledCMCDKeys.filter(key => Constants.CMCD_AVAILABLE_KEYS_EVENT.includes(key));
+ // Add CMCD v2 Event mode mandatory keys
+ const requiredKeys = ['ts', 'sta'];
+ requiredKeys.forEach(key => {
+ if (!enabledCMCDKeys.includes(key)) {
+ enabledCMCDKeys.push(key);
+ }
+ });
+ }
return Object.keys(cmcdData)
.filter(key => enabledCMCDKeys.includes(key))
.reduce((obj, key) => {
@@ -190,19 +382,21 @@ function CmcdModel() {
}
}
- function getHeaderParameters(request) {
+ function getHeaderParameters(request, triggerEvent = true, cmcdReportingMode = null) {
try {
if (isCmcdEnabled()) {
const cmcdData = getCmcdData(request);
- const filteredCmcdData = _applyWhitelist(cmcdData);
+ const filteredCmcdData = _applyWhitelist(cmcdData, cmcdReportingMode);
const headers = toCmcdHeaders(filteredCmcdData)
- eventBus.trigger(MetricsReportingEvents.CMCD_DATA_GENERATED, {
- url: request.url,
- mediaType: request.mediaType,
- cmcdData,
- headers
- });
+ if (triggerEvent) {
+ eventBus.trigger(MetricsReportingEvents.CMCD_DATA_GENERATED, {
+ url: request.url,
+ mediaType: request.mediaType,
+ cmcdData,
+ headers
+ });
+ }
return headers;
}
@@ -271,6 +465,11 @@ function CmcdModel() {
return true;
}
+ function _setCmcdVersion() {
+ const cmcdVersion = settings.get().streaming.cmcd?.version
+ internalData.v = cmcdVersion ? cmcdVersion : CMCD_VERSION;
+ }
+
function getCmcdParametersFromManifest() {
let cmcdParametersFromManifest = {};
if (serviceDescriptionController) {
@@ -359,8 +558,8 @@ function CmcdModel() {
return data;
}
- function _getCmcdDataForMpd() {
- const data = _getGenericCmcdData();
+ function _getCmcdDataForMpd(request) {
+ const data = _getGenericCmcdData(request);
data.ot = CmcdObjectType.MANIFEST;
@@ -369,7 +568,7 @@ function CmcdModel() {
function _getCmcdDataForMediaSegment(request, mediaType) {
_initForMediaType(mediaType);
- const data = _getGenericCmcdData();
+ const data = _getGenericCmcdData(request);
const encodedBitrate = _getBitrateByRequest(request);
const d = _getObjectDurationByRequest(request);
const mtp = _getMeasuredThroughputByType(mediaType);
@@ -454,6 +653,17 @@ function CmcdModel() {
_initialMediaRequestsDone[mediaType] = true;
}
+ // Response timing and status code
+ if (request.startDate && request.firstByteDate){
+ data.ttfb = request.firstByteDate - request.startDate
+ }
+ if (request.endDate && request.startDate){
+ data.ttlb = request.endDate - request.startDate
+ }
+ if (request.status){
+ data.rc = request.status
+ }
+
return data;
}
@@ -472,8 +682,8 @@ function CmcdModel() {
}
}
- function _getCmcdDataForInitSegment() {
- const data = _getGenericCmcdData();
+ function _getCmcdDataForInitSegment(request) {
+ const data = _getGenericCmcdData(request);
data.ot = CmcdObjectType.INIT;
data.su = true;
@@ -481,8 +691,8 @@ function CmcdModel() {
return data;
}
- function _getCmcdDataForOther() {
- const data = _getGenericCmcdData();
+ function _getCmcdDataForOther(request) {
+ const data = _getGenericCmcdData(request);
data.ot = CmcdObjectType.OTHER;
@@ -490,14 +700,18 @@ function CmcdModel() {
}
- function _getGenericCmcdData() {
+ function _getGenericCmcdData(request) {
const cmcdParametersFromManifest = getCmcdParametersFromManifest();
const data = {};
+ if (internalData.sta) {
+ data.sta = internalData.sta;
+ }
+
let cid = settings.get().streaming.cmcd.cid ? settings.get().streaming.cmcd.cid : internalData.cid;
cid = cmcdParametersFromManifest.contentID ? cmcdParametersFromManifest.contentID : cid;
- data.v = CMCD_VERSION;
+ data.v = internalData.v === 2 ? 2 : CMCD_VERSION;
data.sid = settings.get().streaming.cmcd.sid ? settings.get().streaming.cmcd.sid : internalData.sid;
data.sid = cmcdParametersFromManifest.sessionID ? cmcdParametersFromManifest.sessionID : data.sid;
@@ -508,6 +722,21 @@ function CmcdModel() {
data.cid = `${cid}`;
}
+ let int = settings.get().streaming.cmcd.int ? settings.get().streaming.cmcd.int : null;
+ if (int) {
+ data.int = int
+ }
+
+ // Add new ltc and msd cmcd v2 keys
+ let ltc = playbackController.getCurrentLiveLatency() * 1000;
+ if (!isNaN(ltc)) {
+ data.ltc = ltc;
+ }
+
+ if (!isNaN(internalData.msd)) {
+ data.msd = internalData.msd;
+ }
+
if (!isNaN(internalData.pr) && internalData.pr !== 1 && internalData.pr !== null) {
data.pr = internalData.pr;
}
@@ -520,6 +749,17 @@ function CmcdModel() {
data.sf = internalData.sf;
}
+ // Add v2 mandatory keys
+ if (request && internalData.v === 2) {
+ data.url = request.url.split('?')[0]; // remove potential cmcd query params
+ }
+ if (internalData.v === 2) {
+ data.ts = Date.now();
+ }
+ if (internalData.bg){
+ data.bg = internalData.bg
+ }
+
return data;
}
@@ -684,10 +924,23 @@ function CmcdModel() {
}
function reset() {
- eventBus.off(MediaPlayerEvents.PLAYBACK_RATE_CHANGED, _onPlaybackRateChanged, this);
- eventBus.off(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, this);
+ clearTimeout(_eventTimeoutId);
+ eventBus.off(MediaPlayerEvents.PLAYBACK_RATE_CHANGED, _onPlaybackRateChanged, instance);
+ eventBus.off(MediaPlayerEvents.MANIFEST_LOADED, _onManifestLoaded, instance);
eventBus.off(MediaPlayerEvents.BUFFER_LEVEL_STATE_CHANGED, _onBufferLevelStateChanged, instance);
eventBus.off(MediaPlayerEvents.PLAYBACK_SEEKED, _onPlaybackSeeked, instance);
+ eventBus.off(MediaPlayerEvents.PERIOD_SWITCH_COMPLETED, _onPeriodSwitchComplete, instance);
+ eventBus.off(MediaPlayerEvents.PLAYBACK_INITIALIZED, _onPlaybackInitialized, instance);
+ eventBus.off(MediaPlayerEvents.PLAYBACK_STARTED, _onPlaybackStarted, instance);
+ eventBus.off(MediaPlayerEvents.PLAYBACK_PAUSED, _onPlaybackPaused, instance);
+ eventBus.off(MediaPlayerEvents.PLAYBACK_PLAYING, _onPlaybackPlaying, instance)
+ eventBus.off(MediaPlayerEvents.PLAYBACK_SEEKING, _onPlaybackSeeking, instance);
+ eventBus.off(MediaPlayerEvents.PLAYBACK_STALLED, _onPlaybackStalled, instance);
+ eventBus.off(MediaPlayerEvents.PLAYBACK_ERROR, _onPlaybackError, instance);
+ eventBus.off(MediaPlayerEvents.ERROR, _onPlayerError, instance);
+ eventBus.off(MediaPlayerEvents.PLAYBACK_ENDED, _onPlaybackEnded, instance);
+ eventBus.off(MediaPlayerEvents.ALTERNATIVE_PLAYBACK_PLAYING, _onAlternativeStarted, instance);
+ eventBus.off(MediaPlayerEvents.ALTERNATIVE_PLAYBACK_ENDED, _onAlternativeEnded, instance);
_resetInitialSettings();
}
diff --git a/src/streaming/net/HTTPLoader.js b/src/streaming/net/HTTPLoader.js
index e69a0823d8..1e154dc229 100644
--- a/src/streaming/net/HTTPLoader.js
+++ b/src/streaming/net/HTTPLoader.js
@@ -293,6 +293,33 @@ function HTTPLoader(cfg) {
}
});
+ /* CMCD V2 ResponseMode */
+ const cmcdResponseMode = settings.get().streaming.cmcd.reporting.responseMode;
+ if (cmcdModel.isCmcdEnabled() && cmcdResponseMode.enabled) {
+
+ const cmcdMode = cmcdResponseMode.mode ? cmcdResponseMode.mode : settings.get().streaming.cmcd.mode;
+ let requestUrl = cmcdResponseMode.requestUrl;
+ let requestHeaderes = {};
+ const request = httpRequest.customData.request;
+ request.status = httpResponse.status
+
+ if (cmcdMode === Constants.CMCD_MODE_QUERY){
+ const additionalQueryParameter = _getAdditionalQueryParameter(request, false, 2);
+ requestUrl = Utils.addAdditionalQueryParameterToUrl(cmcdResponseMode.requestUrl, additionalQueryParameter);
+ } else if (cmcdMode === Constants.CMCD_MODE_HEADER){
+ requestHeaderes = cmcdModel.getHeaderParameters(request, false, 2);
+ }
+
+ fetch(requestUrl, {
+ method: cmcdResponseMode.requestMethod,
+ headers: requestHeaderes,
+ }).then(response => {
+ console.log('CMCD data sent successfully:', response);
+ }).catch(error => {
+ console.error('Error sending CMCD data:', error);
+ });
+ }
+
};
const _updateRequestTimingInfo = function () {
@@ -624,14 +651,20 @@ function HTTPLoader(cfg) {
const currentAdaptationSetId = request?.mediaInfo?.id?.toString();
const isIncludedFilters = clientDataReportingController.isServiceLocationIncluded(request.type, currentServiceLocation) &&
clientDataReportingController.isAdaptationsIncluded(currentAdaptationSetId);
- if (isIncludedFilters && cmcdModel.isCmcdEnabled()) {
+ const cmcdRequestModeEnabled = settings.get().streaming.cmcd.reporting.requestMode.enabled;
+
+ if (isIncludedFilters && cmcdModel.isCmcdEnabled() && cmcdRequestModeEnabled) {
+ // Needs to be called to trigger the CMCD_DATA_GENERATED event only once
+ // TODO: Check how to generate the event only once
+ cmcdModel.getHeaderParameters(request);
const cmcdParameters = cmcdModel.getCmcdParametersFromManifest();
- const cmcdMode = cmcdParameters.mode ? cmcdParameters.mode : settings.get().streaming.cmcd.mode;
+ const cmcdResponseMode = settings.get().streaming.cmcd.reporting.requestMode.mode;
+ const cmcdMode = cmcdParameters.mode ? cmcdParameters.mode : (cmcdResponseMode ? cmcdResponseMode : settings.get().streaming.cmcd.mode);
if (cmcdMode === Constants.CMCD_MODE_QUERY) {
- const additionalQueryParameter = _getAdditionalQueryParameter(request);
+ const additionalQueryParameter = _getAdditionalQueryParameter(request, false, 1);
request.url = Utils.addAdditionalQueryParameterToUrl(request.url, additionalQueryParameter);
} else if (cmcdMode === Constants.CMCD_MODE_HEADER) {
- request.headers = Object.assign(request.headers, cmcdModel.getHeaderParameters(request));
+ request.headers = Object.assign(request.headers, cmcdModel.getHeaderParameters(request, false, 1));
}
}
}
@@ -642,10 +675,10 @@ function HTTPLoader(cfg) {
* @return {array}
* @private
*/
- function _getAdditionalQueryParameter(request) {
+ function _getAdditionalQueryParameter(request, triggerEvent = true, reportingMode = null) {
try {
const additionalQueryParameter = [];
- const cmcdQueryParameter = cmcdModel.getQueryParameter(request);
+ const cmcdQueryParameter = cmcdModel.getQueryParameter(request, triggerEvent, reportingMode);
if (cmcdQueryParameter) {
additionalQueryParameter.push(cmcdQueryParameter);
diff --git a/src/streaming/utils/CapabilitiesFilter.js b/src/streaming/utils/CapabilitiesFilter.js
index e4a1ecf2c6..1c1c7e4362 100644
--- a/src/streaming/utils/CapabilitiesFilter.js
+++ b/src/streaming/utils/CapabilitiesFilter.js
@@ -172,19 +172,21 @@ function CapabilitiesFilter() {
const configurations = [];
manifest.Period.forEach((period) => {
- period.AdaptationSet.forEach((as) => {
- if (adapter.getIsTypeOf(as, type)) {
- as.Representation.forEach((rep, i) => {
- const codec = adapter.getCodec(as, i, false);
- _processCodecToCheck(type, rep, codec, configurationsSet, configurations);
-
- const supplementalCodecs = adapter.getSupplementalCodecs(rep)
- if (supplementalCodecs.length > 0) {
- _processCodecToCheck(type, rep, supplementalCodecs[0], configurationsSet, configurations);
- }
- });
- }
- });
+ if (!period.ImportedMPD) {
+ period.AdaptationSet.forEach((as) => {
+ if (adapter.getIsTypeOf(as, type)) {
+ as.Representation.forEach((rep, i) => {
+ const codec = adapter.getCodec(as, i, false);
+ _processCodecToCheck(type, rep, codec, configurationsSet, configurations);
+
+ const supplementalCodecs = adapter.getSupplementalCodecs(rep)
+ if (supplementalCodecs.length > 0) {
+ _processCodecToCheck(type, rep, supplementalCodecs[0], configurationsSet, configurations);
+ }
+ });
+ }
+ });
+ }
});
return configurations;
@@ -331,26 +333,28 @@ function CapabilitiesFilter() {
}
manifest.Period.forEach((period) => {
- period.AdaptationSet = period.AdaptationSet.filter((as) => {
+ if (!period.ImportedMPD) {
+ period.AdaptationSet = period.AdaptationSet.filter((as) => {
- if (!as.Representation || as.Representation.length === 0) {
- return true;
- }
+ if (!as.Representation || as.Representation.length === 0) {
+ return true;
+ }
- const adaptationSetEssentialProperties = adapter.getEssentialPropertiesForAdaptationSet(as);
- const doesSupportEssentialProperties = _doesSupportEssentialProperties(adaptationSetEssentialProperties);
+ const adaptationSetEssentialProperties = adapter.getEssentialPropertiesForAdaptationSet(as);
+ const doesSupportEssentialProperties = _doesSupportEssentialProperties(adaptationSetEssentialProperties);
- if (!doesSupportEssentialProperties) {
- return false;
- }
+ if (!doesSupportEssentialProperties) {
+ return false;
+ }
- as.Representation = as.Representation.filter((rep) => {
- const essentialProperties = adapter.getEssentialPropertiesForRepresentation(rep);
- return _doesSupportEssentialProperties(essentialProperties);
- });
+ as.Representation = as.Representation.filter((rep) => {
+ const essentialProperties = adapter.getEssentialPropertiesForRepresentation(rep);
+ return _doesSupportEssentialProperties(essentialProperties);
+ });
- return as.Representation && as.Representation.length > 0;
- });
+ return as.Representation && as.Representation.length > 0;
+ });
+ }
});
}