diff --git a/README.md b/README.md
index 05279cac..41282649 100644
--- a/README.md
+++ b/README.md
@@ -41,12 +41,19 @@ const manifestUri = 'https://example.com/dash.xml';
const res = await fetch(manifestUri);
const manifest = await res.text();
-var parsedManifest = mpdParser.parse(manifest, { manifestUri });
+const { manifest: parsedManifest } = mpdParser.parse(manifest, { manifestUri });
+```
+
+If dealing with a live stream, then on subsequent calls to parse, the last parsed manifest
+object should be provided as an option to `parse` using the `lastMpd` option:
+
+```js
+const parsedManifest = mpdParser.parse(manifest, { manifestUri, lastMpd });
```
### Parsed Output
-The parser ouputs a plain javascript object with the following structure:
+The parser ouputs a parsed manifest as a plain javascript object with the following structure:
```js
Manifest {
@@ -61,6 +68,8 @@ Manifest {
Manifest
}
],
+ playlistToExclude: [],
+ mediaGroupPlaylistsToExclude: []
mediaGroups: {
AUDIO: {
'GROUP-ID': {
@@ -113,6 +122,12 @@ Manifest {
}
```
+For live DASH playlists where a `lastMpd` object was provided, the returned manifest object
+may contain `playlistsToExclude` and `mediaGroupPlaylistsToExclude`. These are arrays
+containing playlists that were found in the `lastMpd` (if provided) but could not be
+matched in the latest MPD. To continue playback uninterrupted, they should be excluded by
+the playback engine.
+
## Including the Parser
To include mpd-parser on your website or web application, use any of the following methods.
@@ -124,8 +139,8 @@ This is the simplest case. Get the script in whatever way you prefer and include
```html
```
@@ -134,16 +149,15 @@ This is the simplest case. Get the script in whatever way you prefer and include
When using with Browserify, install mpd-parser via npm and `require` the parser as you would any other module.
```js
-var mpdParser = require('mpd-parser');
-
-var parsedManifest = mpdParser.parse(manifest, manifestUrl);
+const mpdParser = require('mpd-parser');
+const parsedManifest = mpdParser.parse(manifest, { manifestUri });
```
With ES6:
```js
import { parse } from 'mpd-parser';
-const parsedManifest = parse(manifest, manifestUrl);
+const parsedManifest = parse(manifest, { manifestUri });
```
### RequireJS/AMD
@@ -152,7 +166,7 @@ When using with RequireJS (or another AMD library), get the script in whatever w
```js
require(['mpd-parser'], function(mpdParser) {
- var parsedManifest = mpdParser.parse(manifest, manifestUrl);
+ const parsedManifest = mpdParser.parse(manifest, { manifestUri });
});
```
diff --git a/src/index.js b/src/index.js
index 31924d86..b4cdff8c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -4,15 +4,33 @@ import { toPlaylists } from './toPlaylists';
import { inheritAttributes } from './inheritAttributes';
import { stringToMpdXml } from './stringToMpdXml';
import { parseUTCTimingScheme } from './parseUTCTimingScheme';
-import {addSidxSegmentsToPlaylist} from './segment/segmentBase.js';
+import { addSidxSegmentsToPlaylist } from './segment/segmentBase.js';
const VERSION = version;
+/*
+ * Given a DASH manifest string and options, parses the DASH manifest into a manifest
+ * object.
+ *
+ * For live DASH manifests, if `lastMpd` is provided in options, then the newly parsed
+ * DASH manifest will have its media and discontinuity sequence values updated to reflect
+ * its position relative to the prior manifest.
+ *
+ * @param {string} manifestString - the DASH manifest as a string
+ * @param {options} [options] - any options
+ *
+ * @return {Object} the manifest object
+ */
const parse = (manifestString, options = {}) => {
const parsedManifestInfo = inheritAttributes(stringToMpdXml(manifestString), options);
const playlists = toPlaylists(parsedManifestInfo.representationInfo);
- return toM3u8(playlists, parsedManifestInfo.locations, options.sidxMapping);
+ return toM3u8({
+ dashPlaylists: playlists,
+ locations: parsedManifestInfo.locations,
+ sidxMapping: options.sidxMapping,
+ lastMpd: options.lastMpd
+ });
};
/**
diff --git a/src/inheritAttributes.js b/src/inheritAttributes.js
index 067fb8dc..ad645f18 100644
--- a/src/inheritAttributes.js
+++ b/src/inheritAttributes.js
@@ -1,4 +1,3 @@
-import window from 'global/window';
import { flatten } from './utils/list';
import { merge } from './utils/object';
import { findChildren, getContent } from './utils/xml';
@@ -386,9 +385,20 @@ export const toRepresentations =
*/
export const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, index) => {
const periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period.node, 'BaseURL'));
- const parsedPeriodId = parseInt(period.attributes.id, 10);
- // fallback to mapping index if Period@id is not a number
- const periodIndex = window.isNaN(parsedPeriodId) ? index : parsedPeriodId;
+ // periodIndex used to always use Period@id, and was used as the period's timeline (the
+ // discontinuity sequence for the manifest). This helped address dynamic playlists, as
+ // Period@id was often a number, and it often increased. However, the DASH specification
+ // states that Period@id is a string, and says nothing about increasing values. This led
+ // to problems with dynamic playlists where the Period@id would not be a number and
+ // would not increase for each added period. In addition, when it was a number, it
+ // potentially increased dramatically, making the manifest object harder to understand.
+ //
+ // Now, the index of the period is used, and for overlapping values on refreshes, the
+ // playlist merging logic should handle any increase. This should handle all the
+ // necessary cases we used to support, but may be problematic when it comes time to
+ // handle multiperiod live playlists with SIDX segments (using SegmentBase), as the
+ // merging logic can't yet handle that case. This should be a TODO for the future.
+ const periodIndex = index;
const periodAttributes = merge(mpdAttributes, {
periodIndex,
periodStart: period.attributes.start
diff --git a/src/playlist-merge.js b/src/playlist-merge.js
new file mode 100644
index 00000000..33c3650a
--- /dev/null
+++ b/src/playlist-merge.js
@@ -0,0 +1,586 @@
+import { forEachMediaGroup } from '@videojs/vhs-utils/es/media-groups';
+import {
+ findIndex,
+ findIndexes,
+ includes
+} from './utils/list';
+
+const SUPPORTED_MEDIA_TYPES = ['AUDIO', 'SUBTITLES'];
+
+/**
+ * Finds the playlist with the matching NAME attribute.
+ *
+ * @param {Array} playlists playlists to search through
+ * @param {string} name the NAME attribute to search for
+ *
+ * @return {Object|null} the matching playlist object, or null
+ */
+export const findPlaylistWithName = (playlists, name) => {
+ for (let i = 0; i < playlists.length; i++) {
+ if (playlists[i].attributes.NAME === name) {
+ return playlists[i];
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Finds the media group playlist with the matching NAME attribute.
+ *
+ * @param {Object} config options object
+ * @param {string} config.playlistName the playlist NAME attribute to search for
+ * @param {string} config.type the media group type
+ * @param {string} config.group the media group...group
+ * @param {string} config.label the media group label
+ * @param {Object} config.manifest the main manifest object
+ *
+ * @return {Object|null} the matching media group playlist object, or null
+ */
+export const findMediaGroupPlaylistWithName = ({
+ playlistName,
+ type,
+ group,
+ label,
+ manifest
+}) => {
+ const typeObject = manifest.mediaGroups[type];
+ const properties = typeObject && typeObject[group] && typeObject[group][label];
+
+ if (!properties) {
+ return null;
+ }
+
+ return properties.playlists.length &&
+ findPlaylistWithName(properties.playlists, playlistName);
+};
+
+/**
+ * Returns any old playlists that are no longer available in the new playlists.
+ *
+ * @param {Object} config options object
+ * @param {Array} config.oldPlaylists the old playlists to search within
+ * @param {Array} config.newPlaylists the new playlists to check for
+ *
+ * @return {Array} any playlists not available in new playlists array
+ */
+export const getRemovedPlaylists = ({ oldPlaylists, newPlaylists }) => {
+ // Playlists NAMEs come from DASH Representation IDs, which are mandatory
+ // (see ISO_23009-1-2012 5.3.5.2)
+ const oldNameToPlaylistMap = oldPlaylists.reduce((acc, playlist) => {
+ acc[playlist.attributes.NAME] = playlist;
+ return acc;
+ }, {});
+ const newNameToPlaylistMap = newPlaylists.reduce((acc, playlist) => {
+ acc[playlist.attributes.NAME] = playlist;
+ return acc;
+ }, {});
+
+ return Object.keys(oldNameToPlaylistMap).reduce((acc, oldPlaylistName) => {
+ const oldPlaylist = oldNameToPlaylistMap[oldPlaylistName];
+ const matchingNewPlaylist = newNameToPlaylistMap[oldPlaylistName];
+
+ if (!matchingNewPlaylist) {
+ acc.push(oldPlaylist);
+ }
+
+ return acc;
+ }, []);
+};
+
+/**
+ * Returns any old media group playlists that are no longer available in the new media
+ * group playlists.
+ *
+ * @param {Object} config options object
+ * @param {Object} config.oldManifest the old main manifest object
+ * @param {Object} config.newManifest the new main manifest object
+ *
+ * @return {Array} removed media group playlist objects
+ */
+export const getRemovedMediaGroupPlaylists = ({ oldManifest, newManifest }) => {
+ const removedMediaGroupPlaylists = [];
+
+ forEachMediaGroup(oldManifest, SUPPORTED_MEDIA_TYPES, (properties, type, group, label) => {
+ const oldPlaylists = properties.playlists || [];
+
+ oldPlaylists.forEach((oldPlaylist) => {
+ const newPlaylist = findMediaGroupPlaylistWithName({
+ // Playlists NAMEs come from DASH Representation IDs, which are mandatory
+ // (see ISO_23009-1-2012 5.3.5.2)
+ playlistName: oldPlaylist.attributes.NAME,
+ type,
+ group,
+ label,
+ manifest: newManifest
+ });
+
+ if (!newPlaylist) {
+ removedMediaGroupPlaylists.push({
+ type,
+ group,
+ label,
+ playlist: oldPlaylist
+ });
+ }
+ });
+ });
+
+ return removedMediaGroupPlaylists;
+};
+
+/**
+ * Returns any new playlists that do not account for all timelines (where all timelines is
+ * the max number of timelines seen across all of the passed in playlists).
+ *
+ * @param {Array} playlists the playlists to look through
+ *
+ * @return {Array} any playlists that do not account for all timelines
+ */
+export const getIncompletePlaylists = (playlists) => {
+ const timelines = {};
+ const playlistToTimelines = {};
+
+ playlists.forEach((playlist) => {
+ if (!playlist.segments) {
+ return;
+ }
+
+ const playlistTimelines = playlist.segments.reduce((acc, segment) => {
+ acc[segment.timeline] = true;
+ timelines[segment.timeline] = true;
+
+ return acc;
+ }, {});
+
+ // All playlists should have a unique ID sourced from the required attribute
+ // Representation@id from the MPD.
+ playlistToTimelines[playlist.attributes.NAME] = playlistTimelines;
+ }, {});
+
+ const numNewTimelines = Object.keys(timelines).length;
+
+ if (numNewTimelines === 0) {
+ return [];
+ }
+
+ return playlists.filter((playlist) =>
+ Object.keys(playlistToTimelines[playlist.attributes.NAME]).length < numNewTimelines);
+};
+
+/**
+ * Gets a flattened array of media group playlists.
+ *
+ * @param {Object} manifest the main manifest object
+ *
+ * @return {Array} the media group playlists
+ */
+export const getMediaGroupPlaylists = (manifest) => {
+ let mediaGroupPlaylists = [];
+
+ forEachMediaGroup(manifest, SUPPORTED_MEDIA_TYPES, (properties, type, group, label) => {
+ mediaGroupPlaylists = mediaGroupPlaylists.concat(properties.playlists || []);
+ });
+
+ return mediaGroupPlaylists;
+};
+
+/**
+ * @typedef {Object} MediaGroupPlaylistIdentificationObject
+ * @property {string} type - the media group type
+ * @property {string} group - the media group...group
+ * @property {string} label - the media group label
+ * @property {Object} playlist - the playlist within the group
+ */
+
+/**
+ * Given a list of playlists and a manifest object, returns an array of objects with all
+ * of the necessary media group properties to identify it within a manifest.
+ *
+ * @param {Object} config options object
+ * @param {Array} config.playlists the playlist objects
+ * @param {Object} config.manifest the main manifest object
+ *
+ * @return {MediaGroupPlaylistIdentificationObject[]}
+ * media group playlist identification objects
+ */
+export const getMediaGroupPlaylistIdentificationObjects = ({ playlists, manifest }) => {
+ if (!playlists.length) {
+ return [];
+ }
+
+ const idObjects = [];
+
+ forEachMediaGroup(manifest, SUPPORTED_MEDIA_TYPES, (properties, type, group, label) => {
+ properties.playlists.forEach((playlist) => {
+ if (includes(playlists, playlist)) {
+ idObjects.push({
+ type,
+ group,
+ label,
+ playlist
+ });
+ }
+ });
+ });
+
+ return idObjects;
+};
+
+/**
+ * Goes through the provided segments and updates the appropriate sequence and timeline
+ * related attributes.
+ *
+ * @param {Object} config options object
+ * @param {Array} config.segments the segments to update
+ * @param {number} config.mediaSequenceStart the mediaSequence number to start with
+ * @param {string} config.timelineStart the timeline number to start with
+ */
+export const repositionSegmentsOnTimeline = ({
+ segments,
+ mediaSequenceStart,
+ timelineStart
+}) => {
+ let currentMediaSequence = mediaSequenceStart;
+ let currentTimeline = timelineStart;
+
+ for (let i = 0; i < segments.length; i++) {
+ const segment = segments[i];
+
+ // timelineStart should already account for the first discontinuity
+ if (i > 0 && segment.discontinuity) {
+ currentTimeline++;
+ }
+ segment.number = currentMediaSequence;
+ segment.timeline = currentTimeline;
+ currentMediaSequence++;
+ }
+};
+
+/**
+ * Given a new playlist and an old matching playlist from a prior refresh, updates the new
+ * playlist's sequence and timing values (including segments) to ensure that the new
+ * playlist reflects its relative position to the old one.
+ *
+ * @param {Object} oldPlaylist the old playlist to base the updates on
+ * @param {Object} newPlaylist the new playlist to update
+ */
+export const positionPlaylistOnTimeline = (oldPlaylist, newPlaylist) => {
+ const oldSegments = oldPlaylist.segments || [];
+ const newSegments = newPlaylist.segments || [];
+ // At the time of writing, mpd-parser did not add discontinuitySequence. This code
+ // allows for it to exist or to not exist, and should handle both appropriately.
+ const oldDiscontinuitySequence = oldPlaylist.discontinuitySequence || 0;
+
+ // If the prior playlist had no segments, it's likely either the start of the stream or
+ // a break in the stream. Either way, the best approach is to consider the new segments
+ // a discontinuous region of the media and to continue as if the new playlist is a
+ // continuation of the prior stream.
+ if (oldSegments.length === 0) {
+ newPlaylist.mediaSequence = oldPlaylist.mediaSequence;
+ // The discontinuity sequence should remain the same. Accounting for removed
+ // discontinuities occurs on playlist updates where segments are removed.
+ newPlaylist.discontinuitySequence = oldDiscontinuitySequence;
+ // The timeline should remain the same until a new segment is added.
+ newPlaylist.timeline = oldPlaylist.timeline + (newPlaylist.segments.length ? 1 : 0);
+
+ if (newSegments.length > 0) {
+ newSegments[0].discontinuity = true;
+ }
+
+ repositionSegmentsOnTimeline({
+ segments: newSegments,
+ mediaSequenceStart: newPlaylist.mediaSequence,
+ timelineStart: newPlaylist.timeline
+ });
+
+ newPlaylist.discontinuityStarts = findIndexes(newSegments, 'discontinuity');
+ return;
+ }
+
+ // If there are no new segments then the stream is either reaching an end or a gap.
+ // The media sequence should be updated accordingly, but the timeline should remain the
+ // same, as it will be updated once segments are added.
+ if (newSegments.length === 0) {
+ newPlaylist.mediaSequence = oldPlaylist.mediaSequence + oldSegments.length;
+ newPlaylist.discontinuitySequence =
+ oldDiscontinuitySequence + findIndexes(oldSegments, 'discontinuity').length;
+ newPlaylist.timeline = oldSegments.length ?
+ oldSegments[oldSegments.length - 1].timeline : oldPlaylist.timeline;
+ newPlaylist.discontinuityStarts = [];
+ return;
+ }
+
+ // From here on both old and new playlist have segments.
+ //
+ // To update the new playlist, we must position the old and the new together. The
+ // simplest approach is to try to match an exact segment and adjust timing values from
+ // there. However, this will not always work. It's possible that different periods will
+ // reuse the same segment, and it's also possible that refreshes may change certain
+ // segment properties (e.g., the URI).
+ //
+ // Because of these limitations, the best approach for matching segments is by finding
+ // a segment with a matching presentation time. These values should not change on
+ // refreshes.
+ const firstNewSegment = newSegments[0];
+ const oldMatchingSegmentIndex = findIndex(oldSegments, (oldSegment) =>
+ // allow one 60fps frame as leniency (arbitrarily chosen)
+ Math.abs(oldSegment.presentationTime - firstNewSegment.presentationTime) < (1 / 60));
+ const lastOldSegment = oldSegments[oldSegments.length - 1];
+
+ // No matching segment from the old playlist means the entire playlist was refreshed. In
+ // this case the media sequence should account for this update, and the new segments
+ // should be marked as discontinuous from the prior content, since the last prior period
+ // was removed.
+ if (oldMatchingSegmentIndex < 0) {
+ // It's possible that segments were missed on refresh. However, since we don't have any
+ // access to these prior segments, continue as if the newest segment is the next media
+ // sequence in the stream. At the time of writing, this shouldn't have further
+ // ramifications for playback, as it will be marked as discontinuous with the prior
+ // segments and should offset based on the last buffered values rather than based on
+ // exact DASH manifest values.
+ newPlaylist.mediaSequence = oldPlaylist.mediaSequence + oldSegments.length;
+ newPlaylist.timeline = lastOldSegment.timeline + 1;
+ newPlaylist.discontinuitySequence =
+ oldDiscontinuitySequence + findIndexes(oldSegments, 'discontinuity').length;
+ firstNewSegment.discontinuity = true;
+
+ repositionSegmentsOnTimeline({
+ segments: newSegments,
+ mediaSequenceStart: newPlaylist.mediaSequence,
+ timelineStart: newPlaylist.timeline
+ });
+
+ newPlaylist.discontinuityStarts = findIndexes(newSegments, 'discontinuity');
+ return;
+ }
+
+ // There's a matching segment. Use it to update new playlist segment sequence values.
+ // Note that the matching segment in the new playlist should always be index 0.
+ let oldSegmentIndex = oldMatchingSegmentIndex;
+ let newSegmentIndex = 0;
+
+ // Check if new manifest is start of a period
+ if (oldSegments[oldSegmentIndex].discontinuity) {
+ firstNewSegment.discontinuity = true;
+ }
+
+ while (oldSegmentIndex < oldSegments.length) {
+ const oldSegment = oldSegments[oldSegmentIndex];
+ const newSegment = newSegments[newSegmentIndex];
+
+ // It's possible that segments were removed from the end of the manifest. This may or
+ // may not be legal, as per the spec, but it is seen in practice. It's possible a
+ // situation like this could happen if there are multiple servers providing the
+ // manifest and they aren't perfectly in-sync. If the case is encountered, just break
+ // from this loop.
+ if (!newSegment) {
+ break;
+ }
+
+ newSegment.number = oldSegment.number;
+ newSegment.timeline = oldSegment.timeline;
+
+ oldSegmentIndex++;
+ newSegmentIndex++;
+ }
+
+ newPlaylist.mediaSequence = firstNewSegment.number;
+ newPlaylist.discontinuitySequence = oldDiscontinuitySequence +
+ // since only some of the discontinuities were removed from the playlist, only account
+ // for those
+ findIndexes(oldSegments.slice(0, oldMatchingSegmentIndex), 'discontinuity').length;
+ newPlaylist.timeline = firstNewSegment.timeline;
+ newPlaylist.discontinuityStarts = findIndexes(newSegments, 'discontinuity');
+
+ if (newSegments.length === 1) {
+ return;
+ }
+
+ while (newSegmentIndex > 0 && newSegmentIndex < newSegments.length) {
+ const priorSegment = newSegments[newSegmentIndex - 1];
+ const segment = newSegments[newSegmentIndex];
+
+ segment.number = priorSegment.number + 1;
+ segment.timeline = priorSegment.timeline + (segment.discontinuity ? 1 : 0);
+
+ newSegmentIndex++;
+ }
+};
+
+/**
+ * Given new playlists and old playlists from a prior refresh, updates the new playlists's
+ * sequence and timing values (including segments) to ensure that the new playlists
+ * reflect their relative positions to the old ones.
+ *
+ * Note that this function assumes that the old matching playlists (based on playlist
+ * NAME) exist.
+ *
+ * @param {Array} oldPlaylists the old playlists to base the updates on
+ * @param {Array} newPlaylists the new playlists to update
+ */
+export const positionPlaylistsOnTimeline = ({ oldPlaylists, newPlaylists }) => {
+ newPlaylists.forEach((newPlaylist) => {
+ const playlistName = newPlaylist.attributes.NAME;
+ const oldPlaylist = findPlaylistWithName(oldPlaylists, playlistName);
+
+ if (!oldPlaylist) {
+ return;
+ }
+
+ positionPlaylistOnTimeline(oldPlaylist, newPlaylist);
+ });
+};
+
+/**
+ * Given a new manifest and an old manifest from a prior refresh, updates the new media
+ * group playlists' sequence and timing values (including segments) to ensure that the new
+ * media group playlists reflect their relative positions to the old ones.
+ *
+ * Note that this function assumes that the old matching media group playlists (based on
+ * playlist NAME) exist.
+ *
+ * @param {Object} oldManifest the old main manifest object
+ * @param {Object} newManifest the new main manifest object
+ */
+export const positionMediaGroupPlaylistsOnTimeline = ({ oldManifest, newManifest }) => {
+ forEachMediaGroup(newManifest, SUPPORTED_MEDIA_TYPES, (properties, type, group, label) => {
+ const newPlaylists = properties.playlists || [];
+
+ newPlaylists.forEach((newPlaylist) => {
+ const oldPlaylist = findMediaGroupPlaylistWithName({
+ // Playlists NAMEs come from DASH Representation IDs, which are mandatory
+ // (see ISO_23009-1-2012 5.3.5.2)
+ playlistName: newPlaylist.attributes.NAME,
+ type,
+ group,
+ label,
+ manifest: oldManifest
+ });
+
+ if (!oldPlaylist) {
+ return;
+ }
+
+ positionPlaylistOnTimeline(oldPlaylist, newPlaylist);
+ });
+ });
+};
+
+/**
+ * Removes matching media group playlists from the manifest. This function will also
+ * remove any labels and groups made empty after removal.
+ *
+ * @param {Object} manifest - the manifest object
+ * @param {Object[]} playlists - the media group playlists to remove
+ */
+export const removeMediaGroupPlaylists = ({ manifest, playlists }) => {
+ getMediaGroupPlaylistIdentificationObjects({ playlists, manifest }).forEach((idObject) => {
+ const mediaGroup = manifest.mediaGroups[idObject.type][idObject.group][idObject.label];
+
+ mediaGroup.playlists =
+ mediaGroup.playlists.filter((playlist) => playlist !== idObject.playlist);
+
+ if (mediaGroup.playlists.length === 0) {
+ delete manifest.mediaGroups[idObject.type][idObject.group][idObject.label];
+ }
+
+ if (Object.keys(manifest.mediaGroups[idObject.type][idObject.group]).length === 0) {
+ delete manifest.mediaGroups[idObject.type][idObject.group];
+ }
+ });
+};
+
+/**
+ * Given an old parsed manifest object and a new parsed manifest object, updates the
+ * sequence and timing values within the new manifest to ensure that it lines up with the
+ * old.
+ *
+ * @param {Array} oldManifest the old main manifest object
+ * @param {Array} newManifest the new main manifest object
+ *
+ * @return {Object} the manifest object
+ */
+export const positionManifestOnTimeline = ({ oldManifest, newManifest }) => {
+ const oldPlaylists = oldManifest.playlists;
+ const newPlaylists = newManifest.playlists;
+
+ const incompletePlaylists = getIncompletePlaylists(newPlaylists);
+ const removedPlaylists = getRemovedPlaylists({ oldPlaylists, newPlaylists });
+ // to get added, reverse the sources
+ const addedPlaylists = getRemovedPlaylists({
+ oldPlaylists: newPlaylists,
+ newPlaylists: oldPlaylists
+ });
+
+ const newMediaGroupPlaylists = getMediaGroupPlaylists(newManifest);
+ const incompleteMediaGroupPlaylists = getIncompletePlaylists(newMediaGroupPlaylists);
+ const incompleteMediaGroupPlaylistIdObjects =
+ getMediaGroupPlaylistIdentificationObjects({
+ playlists: incompleteMediaGroupPlaylists,
+ manifest: newManifest
+ });
+ const removedMediaGroupPlaylists = getRemovedMediaGroupPlaylists({
+ oldManifest,
+ newManifest
+ });
+ // to get added, reverse the sources
+ const addedMediaGroupPlaylists = getRemovedMediaGroupPlaylists({
+ oldManifest: newManifest,
+ newManifest: oldManifest
+ });
+
+ // TODO: handle (instead of remove) playlists that only exist for specific timelines
+ // (i.e., incomplete)
+ //
+ // Playlists can be removed from the manifest on period boundaries. For now, we don't
+ // have a good way of handling playlists that exist for only part of a stream. Due to
+ // that, the best course of action is to not include them. In the future, we may want
+ // to have a better way of handling playlists that only represent specific timelines in
+ // the stream.
+ //
+ // If the incomplete playlists are newly added playlists, and on a future refresh will
+ // be complete (for instance, if old periods/timelines are removed on future refreshes
+ // and the new playlist exists for periods/timelines after a certain point) then it will
+ // be available once it is no longer incomplete.
+ newManifest.playlists =
+ newManifest.playlists.filter((playlist) => !includes(incompletePlaylists, playlist));
+ removeMediaGroupPlaylists({
+ manifest: newManifest,
+ playlists: incompleteMediaGroupPlaylists
+ });
+
+ const oldMediaGroupPlaylists = getMediaGroupPlaylists(oldManifest);
+
+ // Only remove newly added playlists if a playlist existed before. This allows for
+ // streams to start empty of playlists and add a first one. However, the behavior may
+ // break if a stream ever removes all playlists and then starts up again adding new
+ // playlists.
+ if (oldMediaGroupPlaylists.length > 0 || oldPlaylists.length > 0) {
+ // TODO: handle (instead of remove) playlists that are added
+ //
+ // Right now, if a playlist is added, it is simply removed from the returned manifest
+ // object. This keeps the merging logic simple, as if there isn't an old playlist to
+ // compare with, then it is harder to position it. However, this can be resolved with
+ // matching presentation times in another playlist (or matching presentation time
+ // ranges within timelines) and sequencing from there.
+ newManifest.playlists =
+ newManifest.playlists.filter((playlist) => !includes(addedPlaylists, playlist));
+ removeMediaGroupPlaylists({
+ manifest: newManifest,
+ playlists: addedMediaGroupPlaylists.map((idObject) => idObject.playlist)
+ });
+ }
+
+ positionPlaylistsOnTimeline({
+ oldPlaylists: oldManifest.playlists,
+ newPlaylists: newManifest.playlists
+ });
+ positionMediaGroupPlaylistsOnTimeline({ oldManifest, newManifest });
+
+ newManifest.playlistsToExclude = removedPlaylists.concat(incompletePlaylists);
+ newManifest.mediaGroupPlaylistsToExclude =
+ removedMediaGroupPlaylists.concat(incompleteMediaGroupPlaylistIdObjects);
+
+ return newManifest;
+};
diff --git a/src/segment/segmentBase.js b/src/segment/segmentBase.js
index eb7397e0..f6aa3f3d 100644
--- a/src/segment/segmentBase.js
+++ b/src/segment/segmentBase.js
@@ -18,7 +18,8 @@ export const segmentsFromBase = (attributes) => {
initialization = {},
sourceDuration,
indexRange = '',
- duration
+ duration,
+ periodStart
} = attributes;
// base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1)
@@ -50,7 +51,9 @@ export const segmentsFromBase = (attributes) => {
segment.timeline = 0;
}
- // This is used for mediaSequence
+ // Since there's only one segment in the period, the segment's presentation time is
+ // the same as the period start.
+ segment.presentationTime = periodStart;
segment.number = 0;
return [segment];
@@ -82,6 +85,10 @@ export const addSidxSegmentsToPlaylist = (playlist, sidx, baseUrl) => {
const mediaReferences = sidx.references.filter(r => r.referenceType !== 1);
const segments = [];
const type = playlist.endList ? 'static' : 'dynamic';
+ // Right now we don't support multiperiod sidx, so we can assume the presentation time
+ // starts at 0. In the future, when multiperiod sidx is handled, the presentation time
+ // will need to account for each period start.
+ let presentationTime = 0;
// firstOffset is the offset from the end of the sidx box
let startIndex = sidxEnd + sidx.firstOffset;
@@ -103,6 +110,10 @@ export const addSidxSegmentsToPlaylist = (playlist, sidx, baseUrl) => {
timeline,
// this is used in parseByDuration
periodIndex: timeline,
+ // Technically the presentationTime isn't the period start, but is instead the
+ // current presentation time for each segment that is added. The segmentsFromBase
+ // function expects period start, so it's renamed to match.
+ periodStart: presentationTime,
duration,
sourceDuration,
indexRange,
@@ -117,6 +128,7 @@ export const addSidxSegmentsToPlaylist = (playlist, sidx, baseUrl) => {
segments.push(segment);
startIndex += size;
+ presentationTime += duration / timescale;
}
playlist.segments = segments;
diff --git a/src/toM3u8.js b/src/toM3u8.js
index e3e1e62a..63d68614 100644
--- a/src/toM3u8.js
+++ b/src/toM3u8.js
@@ -2,6 +2,7 @@ import { values } from './utils/object';
import { findIndexes } from './utils/list';
import { addSidxSegmentsToPlaylist as addSidxSegmentsToPlaylist_ } from './segment/segmentBase';
import { byteRangeToString } from './segment/urlType';
+import { positionManifestOnTimeline } from './playlist-merge';
export const generateSidxKey = (sidx) => sidx &&
sidx.uri + '-' + byteRangeToString(sidx.byterange);
@@ -66,7 +67,13 @@ export const addSidxSegmentsToPlaylists = (playlists, sidxMapping = {}) => {
return playlists;
};
-export const formatAudioPlaylist = ({ attributes, segments, sidx }, isAudioOnly) => {
+export const formatAudioPlaylist = ({
+ attributes,
+ segments,
+ sidx,
+ mediaSequence,
+ discontinuitySequence
+}, isAudioOnly) => {
const playlist = {
attributes: {
NAME: attributes.id,
@@ -79,8 +86,9 @@ export const formatAudioPlaylist = ({ attributes, segments, sidx }, isAudioOnly)
timeline: attributes.periodIndex,
resolvedUri: '',
targetDuration: attributes.duration,
- segments,
- mediaSequence: segments.length ? segments[0].number : 1
+ discontinuitySequence,
+ mediaSequence,
+ segments
};
if (attributes.contentProtection) {
@@ -99,7 +107,12 @@ export const formatAudioPlaylist = ({ attributes, segments, sidx }, isAudioOnly)
return playlist;
};
-export const formatVttPlaylist = ({ attributes, segments }) => {
+export const formatVttPlaylist = ({
+ attributes,
+ segments,
+ mediaSequence,
+ discontinuitySequence
+}) => {
if (typeof segments === 'undefined') {
// vtt tracks may use single file in BaseURL
segments = [{
@@ -107,6 +120,18 @@ export const formatVttPlaylist = ({ attributes, segments }) => {
timeline: attributes.periodIndex,
resolvedUri: attributes.baseUrl || '',
duration: attributes.sourceDuration,
+ // From DASH IOP v4.3 section 6.4.5:
+ //
+ // Only one file for the full period is permitted, practically limiting this use
+ // case to non-live content.
+ //
+ // Such external files are assumed to have a timeline aligned with the Period,
+ // so that TTML time 00:00:00.000 corresponds to the start of the Period. The
+ // presentation time offset is expected to be not presented, and if present,
+ // expected to be ignored by the DASH client.
+ //
+ // The same applies to side-loaded WebVTT files.
+ presentationTime: attributes.periodStart,
number: 0
}];
// targetDuration should be the same duration as the only segment
@@ -129,8 +154,9 @@ export const formatVttPlaylist = ({ attributes, segments }) => {
timeline: attributes.periodIndex,
resolvedUri: attributes.baseUrl || '',
targetDuration: attributes.duration,
- segments,
- mediaSequence: segments.length ? segments[0].number : 1
+ discontinuitySequence,
+ mediaSequence,
+ segments
};
};
@@ -253,8 +279,7 @@ export const formatVideoPlaylist = ({ attributes, segments, sidx }) => {
timeline: attributes.periodIndex,
resolvedUri: '',
targetDuration: attributes.duration,
- segments,
- mediaSequence: segments.length ? segments[0].number : 1
+ segments
};
if (attributes.contentProtection) {
@@ -275,7 +300,41 @@ const audioOnly = ({ attributes }) =>
const vttOnly = ({ attributes }) =>
attributes.mimeType === 'text/vtt' || attributes.contentType === 'text';
-export const toM3u8 = (dashPlaylists, locations, sidxMapping = {}) => {
+/**
+ * Adds appropriate media and discontinuity sequence values to the segments and playlists.
+ *
+ * Throughout mpd-parser, the `number` attribute is used in relation to `startNumber`, a
+ * DASH specific attribute used in constructing segment URI's from templates. However, from
+ * an HLS perspective, the `number` attribute on a segment would be its `mediaSequence`
+ * value, which should start at the original media sequence value (or 0) and increment by 1
+ * for each segment thereafter. Since DASH's `startNumber` values are independent per
+ * period, it doesn't make sense to use it for `number`. Instead, assume everything starts
+ * from a 0 mediaSequence value and increment from there.
+ *
+ * Note that VHS currently doesn't use the `number` property, but it can be helpful for
+ * debugging and making sense of the manifest.
+ *
+ * For live playlists, to account for values increasing in manifests when periods are
+ * removed on refreshes, merging logic should be used to update the numbers to their
+ * appropriate values (to ensure they're sequential and increasing).
+ */
+export const addMediaSequenceValues = (playlists) => {
+ // increment all segments sequentially
+ playlists.forEach((playlist) => {
+ playlist.mediaSequence = 0;
+ playlist.discontinuitySequence = 0;
+
+ if (!playlist.segments) {
+ return;
+ }
+
+ for (let i = 0; i < playlist.segments.length; i++) {
+ playlist.segments[i].number = i;
+ }
+ });
+};
+
+export const toM3u8 = ({ dashPlaylists, locations, sidxMapping = {}, lastMpd }) => {
if (!dashPlaylists.length) {
return {};
}
@@ -293,6 +352,8 @@ export const toM3u8 = (dashPlaylists, locations, sidxMapping = {}) => {
const vttPlaylists = dashPlaylists.filter(vttOnly);
const captions = dashPlaylists.map((playlist) => playlist.attributes.captionServices).filter(Boolean);
+ [videoPlaylists, audioPlaylists, vttPlaylists].forEach(addMediaSequenceValues);
+
const manifest = {
allowCache: true,
discontinuityStarts: [],
@@ -336,5 +397,12 @@ export const toM3u8 = (dashPlaylists, locations, sidxMapping = {}) => {
manifest.mediaGroups['CLOSED-CAPTIONS'].cc = organizeCaptionServices(captions);
}
+ if (lastMpd) {
+ return positionManifestOnTimeline({
+ oldManifest: lastMpd,
+ newManifest: manifest
+ });
+ }
+
return manifest;
};
diff --git a/src/toPlaylists.js b/src/toPlaylists.js
index e12732b7..57679fe5 100644
--- a/src/toPlaylists.js
+++ b/src/toPlaylists.js
@@ -10,6 +10,12 @@ export const generateSegments = ({ attributes, segmentInfo }) => {
if (segmentInfo.template) {
segmentsFn = segmentsFromTemplate;
segmentAttributes = merge(attributes, segmentInfo.template);
+ // TODO: Decide if SegmentList be chosen ahead of SegmentBase.
+ //
+ // Judging by the specification (see the 2012 specification, section 5.3.9.2),
+ // SegmentBase seems to only be used as the strategy if there's no SegmentTemplate or
+ // SegmentList. However, the historical ordering here will use SegmentBase as the
+ // strategy before SegmentList. This ordering should be reconsidered.
} else if (segmentInfo.base) {
segmentsFn = segmentsFromBase;
segmentAttributes = merge(attributes, segmentInfo.base);
diff --git a/src/utils/list.js b/src/utils/list.js
index d3c7d370..f236df9d 100644
--- a/src/utils/list.js
+++ b/src/utils/list.js
@@ -31,3 +31,37 @@ export const findIndexes = (l, key) => l.reduce((a, e, i) => {
return a;
}, []);
+
+/**
+ * Returns the first index that satisfies the matching function, or -1 if not found.
+ *
+ * Only necessary because of IE11 support.
+ *
+ * @param {Array} list the list to search through
+ * @param {Function} matchingFunction the matching function
+ *
+ * @return {number} the matching index or -1 if not found
+ */
+export const findIndex = (list, matchingFunction) => {
+ for (let i = 0; i < list.length; i++) {
+ if (matchingFunction(list[i])) {
+ return i;
+ }
+ }
+
+ return -1;
+};
+
+/**
+ * Returns whether the list contains the search element.
+ *
+ * Only necessary because of IE11 support.
+ *
+ * @param {Array} list the list to search through
+ * @param {*} searchElement the element to look for
+ *
+ * @return {boolean} whether the list includes the search element or not
+ */
+export const includes = (list, searchElement) => {
+ return list.some((element) => element === searchElement);
+};
diff --git a/test/index.test.js b/test/index.test.js
index f3dabd1a..17aa72bc 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -18,6 +18,9 @@ import multiperiodSegmentTemplate from './manifests/multiperiod-segment-template
import multiperiodSegmentList from './manifests/multiperiod-segment-list.mpd';
import multiperiodDynamic from './manifests/multiperiod-dynamic.mpd';
import audioOnly from './manifests/audio-only.mpd';
+import multiperiodStartnumber from './manifests/multiperiod-startnumber.mpd';
+import multiperiodStartnumberRemovedPeriods from
+ './manifests/multiperiod-startnumber-removed-periods.mpd';
import {
parsedManifest as maatVttSegmentTemplateManifest
} from './manifests/maat_vtt_segmentTemplate.js';
@@ -54,6 +57,12 @@ import {
import {
parsedManifest as locationsManifest
} from './manifests/locations.js';
+import {
+ parsedManifest as multiperiodStartnumberManifest
+} from './manifests/multiperiod-startnumber.js';
+import {
+ parsedManifest as multiperiodStartnumberRemovedPeriodsManifest
+} from './manifests/multiperiod-startnumber-removed-periods.js';
import {
parsedManifest as vttCodecsManifest
@@ -129,6 +138,10 @@ QUnit.test('has parse', function(assert) {
name: 'audio-only',
input: audioOnly,
expected: audioOnlyManifest
+}, {
+ name: 'multiperiod_startnumber',
+ input: multiperiodStartnumber,
+ expected: multiperiodStartnumberManifest
}].forEach(({ name, input, expected }) => {
QUnit.test(`${name} test manifest`, function(assert) {
const actual = parse(input);
@@ -136,3 +149,16 @@ QUnit.test('has parse', function(assert) {
assert.deepEqual(actual, expected);
});
});
+
+// this test is handled separately as a `lastMpd` needs to be parsed and provided
+QUnit.test('multiperiod_startnumber_removed_periods test manifest', function(assert) {
+ const lastMpd = parse(multiperiodStartnumber);
+ const actual = parse(multiperiodStartnumberRemovedPeriods, { lastMpd });
+
+ // Since these properties are only added if `lastMpd` is provided in the call to parse,
+ // they should be added here, rather than in the parsed manifest file.
+ multiperiodStartnumberRemovedPeriodsManifest.playlistsToExclude = [];
+ multiperiodStartnumberRemovedPeriodsManifest.mediaGroupPlaylistsToExclude = [];
+
+ assert.deepEqual(actual, multiperiodStartnumberRemovedPeriodsManifest);
+});
diff --git a/test/manifests/608-captions.js b/test/manifests/608-captions.js
index 10061783..e225cfe3 100644
--- a/test/manifests/608-captions.js
+++ b/test/manifests/608-captions.js
@@ -48,11 +48,13 @@ export const parsedManifest = {
resolvedUri: '',
targetDuration: 6,
mediaSequence: 0,
+ discontinuitySequence: 0,
segments: [
{
duration: 6,
timeline: 0,
number: 0,
+ presentationTime: 0,
map: {
uri: '',
resolvedUri: 'https://www.example.com/1080p.ts'
diff --git a/test/manifests/708-captions.js b/test/manifests/708-captions.js
index eae5dfd0..693340af 100644
--- a/test/manifests/708-captions.js
+++ b/test/manifests/708-captions.js
@@ -49,11 +49,13 @@ export const parsedManifest = {
resolvedUri: '',
targetDuration: 6,
mediaSequence: 0,
+ discontinuitySequence: 0,
segments: [
{
duration: 6,
timeline: 0,
number: 0,
+ presentationTime: 0,
map: {
uri: '',
resolvedUri: 'https://www.example.com/1080p.ts'
diff --git a/test/manifests/audio-only.js b/test/manifests/audio-only.js
index 74ecc873..c6d80963 100644
--- a/test/manifests/audio-only.js
+++ b/test/manifests/audio-only.js
@@ -26,7 +26,8 @@ export const parsedManifest = {
resolvedUri: '',
targetDuration: 60,
segments: [],
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
sidx: {
uri: 'http://example.com/audio_en_2c_128k_aac.mp4',
resolvedUri: 'http://example.com/audio_en_2c_128k_aac.mp4',
@@ -44,6 +45,7 @@ export const parsedManifest = {
},
duration: 60,
timeline: 0,
+ presentationTime: 0,
number: 0
}
}
@@ -70,7 +72,8 @@ export const parsedManifest = {
resolvedUri: '',
targetDuration: 60,
segments: [],
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
sidx: {
uri: 'http://example.com/audio_es_2c_128k_aac.mp4',
resolvedUri: 'http://example.com/audio_es_2c_128k_aac.mp4',
@@ -88,6 +91,7 @@ export const parsedManifest = {
},
duration: 60,
timeline: 0,
+ presentationTime: 0,
number: 0
}
}
diff --git a/test/manifests/location.js b/test/manifests/location.js
index 5108de7e..6fb13529 100644
--- a/test/manifests/location.js
+++ b/test/manifests/location.js
@@ -30,11 +30,13 @@ export const parsedManifest = {
resolvedUri: '',
targetDuration: 6,
mediaSequence: 0,
+ discontinuitySequence: 0,
segments: [
{
duration: 6,
timeline: 0,
number: 0,
+ presentationTime: 0,
map: {
uri: '',
resolvedUri: 'https://www.example.com/1080p.ts'
diff --git a/test/manifests/locations.js b/test/manifests/locations.js
index d3e40760..df741de8 100644
--- a/test/manifests/locations.js
+++ b/test/manifests/locations.js
@@ -31,11 +31,13 @@ export const parsedManifest = {
resolvedUri: '',
targetDuration: 6,
mediaSequence: 0,
+ discontinuitySequence: 0,
segments: [
{
duration: 6,
timeline: 0,
number: 0,
+ presentationTime: 0,
map: {
uri: '',
resolvedUri: 'https://www.example.com/1080p.ts'
diff --git a/test/manifests/maat_vtt_segmentTemplate.js b/test/manifests/maat_vtt_segmentTemplate.js
index ec06727d..3c7a38ff 100644
--- a/test/manifests/maat_vtt_segmentTemplate.js
+++ b/test/manifests/maat_vtt_segmentTemplate.js
@@ -74,6 +74,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -146,6 +147,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -226,6 +228,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -298,6 +301,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -338,10 +342,12 @@ export const parsedManifest = {
timeline: 0,
resolvedUri: 'https://example.com/en.vtt',
duration: 6,
+ presentationTime: 0,
number: 0
}
],
- mediaSequence: 0
+ mediaSequence: 0,
+ discontinuitySequence: 0
}
],
uri: ''
@@ -368,10 +374,12 @@ export const parsedManifest = {
timeline: 0,
resolvedUri: 'https://example.com/es.vtt',
duration: 6,
+ presentationTime: 0,
number: 0
}
],
- mediaSequence: 0
+ mediaSequence: 0,
+ discontinuitySequence: 0
}
],
uri: ''
@@ -451,6 +459,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -529,6 +538,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
diff --git a/test/manifests/multiperiod-dynamic.js b/test/manifests/multiperiod-dynamic.js
index 9cab1fda..df926b5f 100644
--- a/test/manifests/multiperiod-dynamic.js
+++ b/test/manifests/multiperiod-dynamic.js
@@ -270,6 +270,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -538,6 +539,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -824,6 +826,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -1098,6 +1101,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -1372,6 +1376,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
diff --git a/test/manifests/multiperiod-segment-list.js b/test/manifests/multiperiod-segment-list.js
index f60dddde..01484f5b 100644
--- a/test/manifests/multiperiod-segment-list.js
+++ b/test/manifests/multiperiod-segment-list.js
@@ -24,7 +24,8 @@ export const parsedManifest = {
'SUBTITLES': 'subs'
},
endList: true,
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
targetDuration: 3,
resolvedUri: '',
segments: [
@@ -38,7 +39,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 0,
uri: 'low/segment-1.ts',
- number: 1
+ number: 0
},
{
duration: 3,
@@ -50,7 +51,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 3,
uri: 'low/segment-2.ts',
- number: 2
+ number: 1
},
{
discontinuity: true,
@@ -63,7 +64,7 @@ export const parsedManifest = {
timeline: 1,
presentationTime: 6,
uri: 'low/segment-1.ts',
- number: 1
+ number: 2
},
{
duration: 3,
@@ -75,7 +76,7 @@ export const parsedManifest = {
timeline: 1,
presentationTime: 9,
uri: 'low/segment-2.ts',
- number: 2
+ number: 3
}
],
timeline: 0,
diff --git a/test/manifests/multiperiod-segment-template.js b/test/manifests/multiperiod-segment-template.js
index f5dece29..7f847022 100644
--- a/test/manifests/multiperiod-segment-template.js
+++ b/test/manifests/multiperiod-segment-template.js
@@ -22,13 +22,13 @@ export const parsedManifest = {
},
uri: '',
endList: true,
- timeline: 1,
+ timeline: 0,
resolvedUri: '',
targetDuration: 5,
segments: [
{
uri: 'audio/segment_0.m4f',
- timeline: 1,
+ timeline: 0,
duration: 5,
resolvedUri: 'https://www.example.com/audio/segment_0.m4f',
map: {
@@ -40,7 +40,7 @@ export const parsedManifest = {
},
{
uri: 'audio/segment_1.m4f',
- timeline: 1,
+ timeline: 0,
duration: 5,
resolvedUri: 'https://www.example.com/audio/segment_1.m4f',
map: {
@@ -52,7 +52,7 @@ export const parsedManifest = {
},
{
uri: 'audio/segment_2.m4f',
- timeline: 1,
+ timeline: 0,
duration: 5,
resolvedUri: 'https://www.example.com/audio/segment_2.m4f',
map: {
@@ -65,42 +65,43 @@ export const parsedManifest = {
{
discontinuity: true,
uri: 'audio/segment_0.m4f',
- timeline: 2,
+ timeline: 1,
duration: 5,
resolvedUri: 'https://www.example.com/audio/segment_0.m4f',
map: {
uri: 'audio/init.m4f',
resolvedUri: 'https://www.example.com/audio/init.m4f'
},
- number: 0,
+ number: 3,
presentationTime: 15
},
{
uri: 'audio/segment_1.m4f',
- timeline: 2,
+ timeline: 1,
duration: 5,
resolvedUri: 'https://www.example.com/audio/segment_1.m4f',
map: {
uri: 'audio/init.m4f',
resolvedUri: 'https://www.example.com/audio/init.m4f'
},
- number: 1,
+ number: 4,
presentationTime: 20
},
{
uri: 'audio/segment_2.m4f',
- timeline: 2,
+ timeline: 1,
duration: 5,
resolvedUri: 'https://www.example.com/audio/segment_2.m4f',
map: {
uri: 'audio/init.m4f',
resolvedUri: 'https://www.example.com/audio/init.m4f'
},
- number: 2,
+ number: 5,
presentationTime: 25
}
],
- mediaSequence: 0
+ mediaSequence: 0,
+ discontinuitySequence: 0
}
],
uri: ''
@@ -127,13 +128,13 @@ export const parsedManifest = {
},
uri: '',
endList: true,
- timeline: 1,
+ timeline: 0,
resolvedUri: '',
targetDuration: 5,
segments: [
{
uri: 'video/segment_0.m4f',
- timeline: 1,
+ timeline: 0,
duration: 5,
resolvedUri: 'https://www.example.com/video/segment_0.m4f',
map: {
@@ -145,7 +146,7 @@ export const parsedManifest = {
},
{
uri: 'video/segment_1.m4f',
- timeline: 1,
+ timeline: 0,
duration: 5,
resolvedUri: 'https://www.example.com/video/segment_1.m4f',
map: {
@@ -157,7 +158,7 @@ export const parsedManifest = {
},
{
uri: 'video/segment_2.m4f',
- timeline: 1,
+ timeline: 0,
duration: 5,
resolvedUri: 'https://www.example.com/video/segment_2.m4f',
map: {
@@ -170,42 +171,43 @@ export const parsedManifest = {
{
discontinuity: true,
uri: 'video/segment_0.m4f',
- timeline: 2,
+ timeline: 1,
duration: 5,
resolvedUri: 'https://www.example.com/video/segment_0.m4f',
map: {
uri: 'video/init.m4f',
resolvedUri: 'https://www.example.com/video/init.m4f'
},
- number: 0,
+ number: 3,
presentationTime: 15
},
{
uri: 'video/segment_1.m4f',
- timeline: 2,
+ timeline: 1,
duration: 5,
resolvedUri: 'https://www.example.com/video/segment_1.m4f',
map: {
uri: 'video/init.m4f',
resolvedUri: 'https://www.example.com/video/init.m4f'
},
- number: 1,
+ number: 4,
presentationTime: 20
},
{
uri: 'video/segment_2.m4f',
- timeline: 2,
+ timeline: 1,
duration: 5,
resolvedUri: 'https://www.example.com/video/segment_2.m4f',
map: {
uri: 'video/init.m4f',
resolvedUri: 'https://www.example.com/video/init.m4f'
},
- number: 2,
+ number: 5,
presentationTime: 25
}
],
- mediaSequence: 0
+ mediaSequence: 0,
+ discontinuitySequence: 0
}
]
};
diff --git a/test/manifests/multiperiod-startnumber-removed-periods.js b/test/manifests/multiperiod-startnumber-removed-periods.js
new file mode 100644
index 00000000..afd11fed
--- /dev/null
+++ b/test/manifests/multiperiod-startnumber-removed-periods.js
@@ -0,0 +1,450 @@
+export const parsedManifest = {
+ allowCache: true,
+ discontinuityStarts: [],
+ duration: 0,
+ endList: true,
+ mediaGroups: {
+ 'AUDIO': {
+ audio: {
+ 'en (main)': {
+ autoselect: true,
+ default: true,
+ language: 'en',
+ playlists: [
+ {
+ attributes: {
+ 'BANDWIDTH': 129262,
+ 'CODECS': 'mp4a.40.5',
+ 'NAME': 'v0',
+ 'PROGRAM-ID': 1
+ },
+ endList: false,
+ mediaSequence: 7,
+ discontinuitySequence: 2,
+ discontinuityStarts: [0],
+ resolvedUri: '',
+ segments: [
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/audio/v0/init.mp4',
+ uri: 'init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/audio/v0/862.m4f',
+ timeline: 3,
+ uri: '862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/audio/v0/init.mp4',
+ uri: 'init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/audio/v0/863.m4f',
+ timeline: 3,
+ uri: '863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/audio/v0/init.mp4',
+ uri: 'init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/audio/v0/864.m4f',
+ timeline: 3,
+ uri: '864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 3,
+ uri: ''
+ }
+ ],
+ uri: ''
+ }
+ }
+ },
+ 'CLOSED-CAPTIONS': {},
+ 'SUBTITLES': {},
+ 'VIDEO': {}
+ },
+ minimumUpdatePeriod: 2000,
+ playlists: [
+ {
+ attributes: {
+ 'AUDIO': 'audio',
+ 'BANDWIDTH': 2942295,
+ 'CODECS': 'avc1.4d001f',
+ 'NAME': 'D',
+ 'PROGRAM-ID': 1,
+ 'RESOLUTION': {
+ height: 720,
+ width: 1280
+ },
+ 'SUBTITLES': 'subs'
+ },
+ endList: false,
+ mediaSequence: 7,
+ discontinuitySequence: 2,
+ discontinuityStarts: [0],
+ resolvedUri: '',
+ segments: [
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/D/D_init.mp4',
+ uri: 'D_init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/video/D/D862.m4f',
+ timeline: 3,
+ uri: 'D862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/D/D_init.mp4',
+ uri: 'D_init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/video/D/D863.m4f',
+ timeline: 3,
+ uri: 'D863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/D/D_init.mp4',
+ uri: 'D_init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/video/D/D864.m4f',
+ timeline: 3,
+ uri: 'D864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 3,
+ uri: ''
+ },
+ {
+ attributes: {
+ 'AUDIO': 'audio',
+ 'BANDWIDTH': 4267536,
+ 'CODECS': 'avc1.640020',
+ 'NAME': 'E',
+ 'PROGRAM-ID': 1,
+ 'RESOLUTION': {
+ height: 720,
+ width: 1280
+ },
+ 'SUBTITLES': 'subs'
+ },
+ endList: false,
+ mediaSequence: 7,
+ discontinuitySequence: 2,
+ discontinuityStarts: [0],
+ resolvedUri: '',
+ segments: [
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/E_init.mp4',
+ uri: 'E_init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/video/E/E862.m4f',
+ timeline: 3,
+ uri: 'E862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/E_init.mp4',
+ uri: 'E_init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/video/E/E863.m4f',
+ timeline: 3,
+ uri: 'E863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/E_init.mp4',
+ uri: 'E_init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/video/E/E864.m4f',
+ timeline: 3,
+ uri: 'E864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 3,
+ uri: ''
+ },
+ {
+ attributes: {
+ 'AUDIO': 'audio',
+ 'BANDWIDTH': 5256859,
+ 'CODECS': 'avc1.640020',
+ 'NAME': 'F',
+ 'PROGRAM-ID': 1,
+ 'RESOLUTION': {
+ height: 720,
+ width: 1280
+ },
+ 'SUBTITLES': 'subs'
+ },
+ endList: false,
+ mediaSequence: 7,
+ discontinuitySequence: 2,
+ discontinuityStarts: [0],
+ resolvedUri: '',
+ segments: [
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/F/F_init.mp4',
+ uri: 'F_init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/video/F/F862.m4f',
+ timeline: 3,
+ uri: 'F862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/F/F_init.mp4',
+ uri: 'F_init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/video/F/F863.m4f',
+ timeline: 3,
+ uri: 'F863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/F/F_init.mp4',
+ uri: 'F_init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/video/F/F864.m4f',
+ timeline: 3,
+ uri: 'F864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 3,
+ uri: ''
+ },
+ {
+ attributes: {
+ 'AUDIO': 'audio',
+ 'BANDWIDTH': 240781,
+ 'CODECS': 'avc1.4d000d',
+ 'NAME': 'A',
+ 'PROGRAM-ID': 1,
+ 'RESOLUTION': {
+ height: 234,
+ width: 416
+ },
+ 'SUBTITLES': 'subs'
+ },
+ endList: false,
+ mediaSequence: 7,
+ discontinuitySequence: 2,
+ discontinuityStarts: [0],
+ resolvedUri: '',
+ segments: [
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/A/A_init.mp4',
+ uri: 'A_init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/video/A/A862.m4f',
+ timeline: 3,
+ uri: 'A862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/A/A_init.mp4',
+ uri: 'A_init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/video/A/A863.m4f',
+ timeline: 3,
+ uri: 'A863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/A/A_init.mp4',
+ uri: 'A_init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/video/A/A864.m4f',
+ timeline: 3,
+ uri: 'A864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 3,
+ uri: ''
+ },
+ {
+ attributes: {
+ 'AUDIO': 'audio',
+ 'BANDWIDTH': 494354,
+ 'CODECS': 'avc1.4d001e',
+ 'NAME': 'B',
+ 'PROGRAM-ID': 1,
+ 'RESOLUTION': {
+ height: 360,
+ width: 640
+ },
+ 'SUBTITLES': 'subs'
+ },
+ endList: false,
+ mediaSequence: 7,
+ discontinuitySequence: 2,
+ discontinuityStarts: [0],
+ resolvedUri: '',
+ segments: [
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/B/B_init.mp4',
+ uri: 'B_init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/video/B/B862.m4f',
+ timeline: 3,
+ uri: 'B862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/B/B_init.mp4',
+ uri: 'B_init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/video/B/B863.m4f',
+ timeline: 3,
+ uri: 'B863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/B/B_init.mp4',
+ uri: 'B_init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/video/B/B864.m4f',
+ timeline: 3,
+ uri: 'B864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 3,
+ uri: ''
+ },
+ {
+ attributes: {
+ 'AUDIO': 'audio',
+ 'BANDWIDTH': 1277155,
+ 'CODECS': 'avc1.4d001f',
+ 'NAME': 'C',
+ 'PROGRAM-ID': 1,
+ 'RESOLUTION': {
+ height: 540,
+ width: 960
+ },
+ 'SUBTITLES': 'subs'
+ },
+ endList: false,
+ mediaSequence: 7,
+ discontinuitySequence: 2,
+ discontinuityStarts: [0],
+ resolvedUri: '',
+ segments: [
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/C/C_init.mp4',
+ uri: 'C_init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/video/C/C862.m4f',
+ timeline: 3,
+ uri: 'C862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/C/C_init.mp4',
+ uri: 'C_init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/video/C/C863.m4f',
+ timeline: 3,
+ uri: 'C863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/C/C_init.mp4',
+ uri: 'C_init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/video/C/C864.m4f',
+ timeline: 3,
+ uri: 'C864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 3,
+ uri: ''
+ }
+ ],
+ segments: [],
+ suggestedPresentationDelay: 6,
+ uri: ''
+};
diff --git a/test/manifests/multiperiod-startnumber-removed-periods.mpd b/test/manifests/multiperiod-startnumber-removed-periods.mpd
new file mode 100644
index 00000000..df12a207
--- /dev/null
+++ b/test/manifests/multiperiod-startnumber-removed-periods.mpd
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+ http://example.com/audio/v0/
+
+
+
+
+
+
+
+
+
+
+ http://example.com/video/D/
+
+
+
+
+
+
+
+ http://example.com/video/E/
+
+
+
+
+
+
+
+ http://example.com/video/F/
+
+
+
+
+
+
+
+
+
+ http://example.com/video/A/
+
+
+
+
+
+
+
+ http://example.com/video/B/
+
+
+
+
+
+
+
+ http://example.com/video/C/
+
+
+
+
+
+
+
+
+
+
diff --git a/test/manifests/multiperiod-startnumber.js b/test/manifests/multiperiod-startnumber.js
new file mode 100644
index 00000000..2a93d0a6
--- /dev/null
+++ b/test/manifests/multiperiod-startnumber.js
@@ -0,0 +1,1045 @@
+export const parsedManifest = {
+ allowCache: true,
+ discontinuityStarts: [],
+ duration: 0,
+ endList: true,
+ mediaGroups: {
+ 'AUDIO': {
+ audio: {
+ 'en (main)': {
+ autoselect: true,
+ default: true,
+ language: 'en',
+ playlists: [
+ {
+ attributes: {
+ 'BANDWIDTH': 129262,
+ 'CODECS': 'mp4a.40.5',
+ 'NAME': 'v0',
+ 'PROGRAM-ID': 1
+ },
+ endList: false,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ resolvedUri: '',
+ segments: [
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/audio/init.mp4',
+ uri: 'init.mp4'
+ },
+ presentationTime: 100,
+ number: 0,
+ resolvedUri: 'http://example.com/audio/500.m4f',
+ timeline: 0,
+ uri: '500.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/audio/init.mp4',
+ uri: 'init.mp4'
+ },
+ presentationTime: 101,
+ number: 1,
+ resolvedUri: 'http://example.com/audio/501.m4f',
+ timeline: 0,
+ uri: '501.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/audio/init.mp4',
+ uri: 'init.mp4'
+ },
+ presentationTime: 102,
+ number: 2,
+ resolvedUri: 'http://example.com/audio/502.m4f',
+ timeline: 0,
+ uri: '502.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/audio/v0/init.mp4',
+ uri: 'init.mp4'
+ },
+ presentationTime: 103,
+ number: 3,
+ resolvedUri: 'http://example.com/audio/v0/000.m4f',
+ timeline: 1,
+ uri: '000.m4f'
+ },
+ {
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/audio/v0/init.mp4',
+ uri: 'init.mp4'
+ },
+ presentationTime: 105,
+ number: 4,
+ resolvedUri: 'http://example.com/audio/v0/001.m4f',
+ timeline: 1,
+ uri: '001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/audio/v0/init.mp4',
+ uri: 'init.mp4'
+ },
+ presentationTime: 107,
+ number: 5,
+ resolvedUri: 'http://example.com/audio/v0/000.m4f',
+ timeline: 2,
+ uri: '000.m4f'
+ },
+ {
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/audio/v0/init.mp4',
+ uri: 'init.mp4'
+ },
+ presentationTime: 109,
+ number: 6,
+ resolvedUri: 'http://example.com/audio/v0/001.m4f',
+ timeline: 2,
+ uri: '001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/audio/v0/init.mp4',
+ uri: 'init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/audio/v0/862.m4f',
+ timeline: 3,
+ uri: '862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/audio/v0/init.mp4',
+ uri: 'init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/audio/v0/863.m4f',
+ timeline: 3,
+ uri: '863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/audio/v0/init.mp4',
+ uri: 'init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/audio/v0/864.m4f',
+ timeline: 3,
+ uri: '864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 0,
+ uri: ''
+ }
+ ],
+ uri: ''
+ }
+ }
+ },
+ 'CLOSED-CAPTIONS': {},
+ 'SUBTITLES': {},
+ 'VIDEO': {}
+ },
+ minimumUpdatePeriod: 2000,
+ playlists: [
+ {
+ attributes: {
+ 'AUDIO': 'audio',
+ 'BANDWIDTH': 2942295,
+ 'CODECS': 'avc1.4d001f',
+ 'NAME': 'D',
+ 'PROGRAM-ID': 1,
+ 'RESOLUTION': {
+ height: 720,
+ width: 1280
+ },
+ 'SUBTITLES': 'subs'
+ },
+ endList: false,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ resolvedUri: '',
+ segments: [
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/D/D_init.mp4',
+ uri: 'D_init.mp4'
+ },
+ presentationTime: 100,
+ number: 0,
+ resolvedUri: 'http://example.com/video/D/D500.m4f',
+ timeline: 0,
+ uri: 'D500.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/D/D_init.mp4',
+ uri: 'D_init.mp4'
+ },
+ presentationTime: 101,
+ number: 1,
+ resolvedUri: 'http://example.com/video/D/D501.m4f',
+ timeline: 0,
+ uri: 'D501.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/D/D_init.mp4',
+ uri: 'D_init.mp4'
+ },
+ presentationTime: 102,
+ number: 2,
+ resolvedUri: 'http://example.com/video/D/D502.m4f',
+ timeline: 0,
+ uri: 'D502.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/D/D_init.mp4',
+ uri: 'D_init.mp4'
+ },
+ presentationTime: 103,
+ number: 3,
+ resolvedUri: 'http://example.com/video/D/D000.m4f',
+ timeline: 1,
+ uri: 'D000.m4f'
+ },
+ {
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/D/D_init.mp4',
+ uri: 'D_init.mp4'
+ },
+ presentationTime: 105,
+ number: 4,
+ resolvedUri: 'http://example.com/video/D/D001.m4f',
+ timeline: 1,
+ uri: 'D001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/D/D_init.mp4',
+ uri: 'D_init.mp4'
+ },
+ presentationTime: 107,
+ number: 5,
+ resolvedUri: 'http://example.com/video/D/D000.m4f',
+ timeline: 2,
+ uri: 'D000.m4f'
+ },
+ {
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/D/D_init.mp4',
+ uri: 'D_init.mp4'
+ },
+ presentationTime: 109,
+ number: 6,
+ resolvedUri: 'http://example.com/video/D/D001.m4f',
+ timeline: 2,
+ uri: 'D001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/D/D_init.mp4',
+ uri: 'D_init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/video/D/D862.m4f',
+ timeline: 3,
+ uri: 'D862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/D/D_init.mp4',
+ uri: 'D_init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/video/D/D863.m4f',
+ timeline: 3,
+ uri: 'D863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/D/D_init.mp4',
+ uri: 'D_init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/video/D/D864.m4f',
+ timeline: 3,
+ uri: 'D864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 0,
+ uri: ''
+ },
+ {
+ attributes: {
+ 'AUDIO': 'audio',
+ 'BANDWIDTH': 4267536,
+ 'CODECS': 'avc1.640020',
+ 'NAME': 'E',
+ 'PROGRAM-ID': 1,
+ 'RESOLUTION': {
+ height: 720,
+ width: 1280
+ },
+ 'SUBTITLES': 'subs'
+ },
+ endList: false,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ resolvedUri: '',
+ segments: [
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/E_init.mp4',
+ uri: 'E_init.mp4'
+ },
+ presentationTime: 100,
+ number: 0,
+ resolvedUri: 'http://example.com/video/E/E500.m4f',
+ timeline: 0,
+ uri: 'E500.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/E_init.mp4',
+ uri: 'E_init.mp4'
+ },
+ presentationTime: 101,
+ number: 1,
+ resolvedUri: 'http://example.com/video/E/E501.m4f',
+ timeline: 0,
+ uri: 'E501.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/E_init.mp4',
+ uri: 'E_init.mp4'
+ },
+ presentationTime: 102,
+ number: 2,
+ resolvedUri: 'http://example.com/video/E/E502.m4f',
+ timeline: 0,
+ uri: 'E502.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/E/E_init.mp4',
+ uri: 'E_init.mp4'
+ },
+ presentationTime: 103,
+ number: 3,
+ resolvedUri: 'http://example.com/video/E/E000.m4f',
+ timeline: 1,
+ uri: 'E000.m4f'
+ },
+ {
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/E/E_init.mp4',
+ uri: 'E_init.mp4'
+ },
+ presentationTime: 105,
+ number: 4,
+ resolvedUri: 'http://example.com/video/E/E001.m4f',
+ timeline: 1,
+ uri: 'E001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/E/E_init.mp4',
+ uri: 'E_init.mp4'
+ },
+ presentationTime: 107,
+ number: 5,
+ resolvedUri: 'http://example.com/video/E/E000.m4f',
+ timeline: 2,
+ uri: 'E000.m4f'
+ },
+ {
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/E/E_init.mp4',
+ uri: 'E_init.mp4'
+ },
+ presentationTime: 109,
+ number: 6,
+ resolvedUri: 'http://example.com/video/E/E001.m4f',
+ timeline: 2,
+ uri: 'E001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/E_init.mp4',
+ uri: 'E_init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/video/E/E862.m4f',
+ timeline: 3,
+ uri: 'E862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/E_init.mp4',
+ uri: 'E_init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/video/E/E863.m4f',
+ timeline: 3,
+ uri: 'E863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/E_init.mp4',
+ uri: 'E_init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/video/E/E864.m4f',
+ timeline: 3,
+ uri: 'E864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 0,
+ uri: ''
+ },
+ {
+ attributes: {
+ 'AUDIO': 'audio',
+ 'BANDWIDTH': 5256859,
+ 'CODECS': 'avc1.640020',
+ 'NAME': 'F',
+ 'PROGRAM-ID': 1,
+ 'RESOLUTION': {
+ height: 720,
+ width: 1280
+ },
+ 'SUBTITLES': 'subs'
+ },
+ endList: false,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ resolvedUri: '',
+ segments: [
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/F_init.mp4',
+ uri: 'F_init.mp4'
+ },
+ presentationTime: 100,
+ number: 0,
+ resolvedUri: 'http://example.com/video/E/F500.m4f',
+ timeline: 0,
+ uri: 'F500.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/F_init.mp4',
+ uri: 'F_init.mp4'
+ },
+ presentationTime: 101,
+ number: 1,
+ resolvedUri: 'http://example.com/video/E/F501.m4f',
+ timeline: 0,
+ uri: 'F501.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/F_init.mp4',
+ uri: 'F_init.mp4'
+ },
+ presentationTime: 102,
+ number: 2,
+ resolvedUri: 'http://example.com/video/E/F502.m4f',
+ timeline: 0,
+ uri: 'F502.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/F/F_init.mp4',
+ uri: 'F_init.mp4'
+ },
+ presentationTime: 103,
+ number: 3,
+ resolvedUri: 'http://example.com/video/F/F000.m4f',
+ timeline: 1,
+ uri: 'F000.m4f'
+ },
+ {
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/F/F_init.mp4',
+ uri: 'F_init.mp4'
+ },
+ presentationTime: 105,
+ number: 4,
+ resolvedUri: 'http://example.com/video/F/F001.m4f',
+ timeline: 1,
+ uri: 'F001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/F/F_init.mp4',
+ uri: 'F_init.mp4'
+ },
+ presentationTime: 107,
+ number: 5,
+ resolvedUri: 'http://example.com/video/F/F000.m4f',
+ timeline: 2,
+ uri: 'F000.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/F/F_init.mp4',
+ uri: 'F_init.mp4'
+ },
+ presentationTime: 108,
+ number: 6,
+ resolvedUri: 'http://example.com/video/F/F001.m4f',
+ timeline: 2,
+ uri: 'F001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/F/F_init.mp4',
+ uri: 'F_init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/video/F/F862.m4f',
+ timeline: 3,
+ uri: 'F862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/F/F_init.mp4',
+ uri: 'F_init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/video/F/F863.m4f',
+ timeline: 3,
+ uri: 'F863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/F/F_init.mp4',
+ uri: 'F_init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/video/F/F864.m4f',
+ timeline: 3,
+ uri: 'F864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 0,
+ uri: ''
+ },
+ {
+ attributes: {
+ 'AUDIO': 'audio',
+ 'BANDWIDTH': 240781,
+ 'CODECS': 'avc1.4d000d',
+ 'NAME': 'A',
+ 'PROGRAM-ID': 1,
+ 'RESOLUTION': {
+ height: 234,
+ width: 416
+ },
+ 'SUBTITLES': 'subs'
+ },
+ endList: false,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ resolvedUri: '',
+ segments: [
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/A/A_init.mp4',
+ uri: 'A_init.mp4'
+ },
+ presentationTime: 100,
+ number: 0,
+ resolvedUri: 'http://example.com/video/A/A500.m4f',
+ timeline: 0,
+ uri: 'A500.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/A/A_init.mp4',
+ uri: 'A_init.mp4'
+ },
+ presentationTime: 101,
+ number: 1,
+ resolvedUri: 'http://example.com/video/A/A501.m4f',
+ timeline: 0,
+ uri: 'A501.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/A/A_init.mp4',
+ uri: 'A_init.mp4'
+ },
+ presentationTime: 102,
+ number: 2,
+ resolvedUri: 'http://example.com/video/A/A502.m4f',
+ timeline: 0,
+ uri: 'A502.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/A/A_init.mp4',
+ uri: 'A_init.mp4'
+ },
+ presentationTime: 103,
+ number: 3,
+ resolvedUri: 'http://example.com/video/A/A000.m4f',
+ timeline: 1,
+ uri: 'A000.m4f'
+ },
+ {
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/A/A_init.mp4',
+ uri: 'A_init.mp4'
+ },
+ presentationTime: 105,
+ number: 4,
+ resolvedUri: 'http://example.com/video/A/A001.m4f',
+ timeline: 1,
+ uri: 'A001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/A/A_init.mp4',
+ uri: 'A_init.mp4'
+ },
+ presentationTime: 107,
+ number: 5,
+ resolvedUri: 'http://example.com/video/A/A000.m4f',
+ timeline: 2,
+ uri: 'A000.m4f'
+ },
+ {
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/A/A_init.mp4',
+ uri: 'A_init.mp4'
+ },
+ presentationTime: 109,
+ number: 6,
+ resolvedUri: 'http://example.com/video/A/A001.m4f',
+ timeline: 2,
+ uri: 'A001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/A/A_init.mp4',
+ uri: 'A_init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/video/A/A862.m4f',
+ timeline: 3,
+ uri: 'A862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/A/A_init.mp4',
+ uri: 'A_init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/video/A/A863.m4f',
+ timeline: 3,
+ uri: 'A863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/A/A_init.mp4',
+ uri: 'A_init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/video/A/A864.m4f',
+ timeline: 3,
+ uri: 'A864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 0,
+ uri: ''
+ },
+ {
+ attributes: {
+ 'AUDIO': 'audio',
+ 'BANDWIDTH': 494354,
+ 'CODECS': 'avc1.4d001e',
+ 'NAME': 'B',
+ 'PROGRAM-ID': 1,
+ 'RESOLUTION': {
+ height: 360,
+ width: 640
+ },
+ 'SUBTITLES': 'subs'
+ },
+ endList: false,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ resolvedUri: '',
+ segments: [
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/B/B_init.mp4',
+ uri: 'B_init.mp4'
+ },
+ presentationTime: 100,
+ number: 0,
+ resolvedUri: 'http://example.com/video/B/B500.m4f',
+ timeline: 0,
+ uri: 'B500.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/B/B_init.mp4',
+ uri: 'B_init.mp4'
+ },
+ presentationTime: 101,
+ number: 1,
+ resolvedUri: 'http://example.com/video/B/B501.m4f',
+ timeline: 0,
+ uri: 'B501.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/B/B_init.mp4',
+ uri: 'B_init.mp4'
+ },
+ presentationTime: 102,
+ number: 2,
+ resolvedUri: 'http://example.com/video/B/B502.m4f',
+ timeline: 0,
+ uri: 'B502.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/B/B_init.mp4',
+ uri: 'B_init.mp4'
+ },
+ presentationTime: 103,
+ number: 3,
+ resolvedUri: 'http://example.com/video/B/B000.m4f',
+ timeline: 1,
+ uri: 'B000.m4f'
+ },
+ {
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/B/B_init.mp4',
+ uri: 'B_init.mp4'
+ },
+ presentationTime: 105,
+ number: 4,
+ resolvedUri: 'http://example.com/video/B/B001.m4f',
+ timeline: 1,
+ uri: 'B001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/B/B_init.mp4',
+ uri: 'B_init.mp4'
+ },
+ presentationTime: 107,
+ number: 5,
+ resolvedUri: 'http://example.com/video/B/B000.m4f',
+ timeline: 2,
+ uri: 'B000.m4f'
+ },
+ {
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/B/B_init.mp4',
+ uri: 'B_init.mp4'
+ },
+ presentationTime: 109,
+ number: 6,
+ resolvedUri: 'http://example.com/video/B/B001.m4f',
+ timeline: 2,
+ uri: 'B001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/B/B_init.mp4',
+ uri: 'B_init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/video/B/B862.m4f',
+ timeline: 3,
+ uri: 'B862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/B/B_init.mp4',
+ uri: 'B_init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/video/B/B863.m4f',
+ timeline: 3,
+ uri: 'B863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/B/B_init.mp4',
+ uri: 'B_init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/video/B/B864.m4f',
+ timeline: 3,
+ uri: 'B864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 0,
+ uri: ''
+ },
+ {
+ attributes: {
+ 'AUDIO': 'audio',
+ 'BANDWIDTH': 1277155,
+ 'CODECS': 'avc1.4d001e',
+ 'NAME': 'C',
+ 'PROGRAM-ID': 1,
+ 'RESOLUTION': {
+ height: 540,
+ width: 960
+ },
+ 'SUBTITLES': 'subs'
+ },
+ endList: false,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ resolvedUri: '',
+ segments: [
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/C_init.mp4',
+ uri: 'C_init.mp4'
+ },
+ presentationTime: 100,
+ number: 0,
+ resolvedUri: 'http://example.com/video/E/C500.m4f',
+ timeline: 0,
+ uri: 'C500.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/C_init.mp4',
+ uri: 'C_init.mp4'
+ },
+ presentationTime: 101,
+ number: 1,
+ resolvedUri: 'http://example.com/video/E/C501.m4f',
+ timeline: 0,
+ uri: 'C501.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/E/C_init.mp4',
+ uri: 'C_init.mp4'
+ },
+ presentationTime: 102,
+ number: 2,
+ resolvedUri: 'http://example.com/video/E/C502.m4f',
+ timeline: 0,
+ uri: 'C502.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/C/C_init.mp4',
+ uri: 'C_init.mp4'
+ },
+ presentationTime: 103,
+ number: 3,
+ resolvedUri: 'http://example.com/video/C/C000.m4f',
+ timeline: 1,
+ uri: 'C000.m4f'
+ },
+ {
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/C/C_init.mp4',
+ uri: 'C_init.mp4'
+ },
+ presentationTime: 105,
+ number: 4,
+ resolvedUri: 'http://example.com/video/C/C001.m4f',
+ timeline: 1,
+ uri: 'C001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/C/C_init.mp4',
+ uri: 'C_init.mp4'
+ },
+ presentationTime: 107,
+ number: 5,
+ resolvedUri: 'http://example.com/video/C/C000.m4f',
+ timeline: 2,
+ uri: 'C000.m4f'
+ },
+ {
+ duration: 2,
+ map: {
+ resolvedUri: 'http://example.com/video/C/C_init.mp4',
+ uri: 'C_init.mp4'
+ },
+ presentationTime: 109,
+ number: 6,
+ resolvedUri: 'http://example.com/video/C/C001.m4f',
+ timeline: 2,
+ uri: 'C001.m4f'
+ },
+ {
+ discontinuity: true,
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/C/C_init.mp4',
+ uri: 'C_init.mp4'
+ },
+ presentationTime: 111,
+ number: 7,
+ resolvedUri: 'http://example.com/video/C/C862.m4f',
+ timeline: 3,
+ uri: 'C862.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/C/C_init.mp4',
+ uri: 'C_init.mp4'
+ },
+ presentationTime: 112,
+ number: 8,
+ resolvedUri: 'http://example.com/video/C/C863.m4f',
+ timeline: 3,
+ uri: 'C863.m4f'
+ },
+ {
+ duration: 1,
+ map: {
+ resolvedUri: 'http://example.com/video/C/C_init.mp4',
+ uri: 'C_init.mp4'
+ },
+ presentationTime: 113,
+ number: 9,
+ resolvedUri: 'http://example.com/video/C/C864.m4f',
+ timeline: 3,
+ uri: 'C864.m4f'
+ }
+ ],
+ targetDuration: 1,
+ timeline: 0,
+ uri: ''
+ }
+ ],
+ segments: [],
+ suggestedPresentationDelay: 6,
+ uri: ''
+};
diff --git a/test/manifests/multiperiod-startnumber.mpd b/test/manifests/multiperiod-startnumber.mpd
new file mode 100644
index 00000000..6fb927f0
--- /dev/null
+++ b/test/manifests/multiperiod-startnumber.mpd
@@ -0,0 +1,647 @@
+
+
+
+
+
+
+
+ http://example.com/audio/1
+
+
+
+
+
+
+
+
+
+ http://example.com/video/D/
+
+
+
+
+
+
+
+ http://example.com/video/E/
+
+
+
+
+
+
+
+ http://example.com/video/E/
+
+
+
+
+
+
+
+
+
+ http://example.com/video/A/
+
+
+
+
+
+
+
+ http://example.com/video/B/
+
+
+
+
+
+
+
+ http://example.com/video/E/
+
+
+
+
+
+
+
+
+
+
+
+
+
+ http://example.com/audio/v0/
+
+
+
+
+
+
+
+
+
+ http://example.com/video/D/
+
+
+
+
+
+
+
+ http://example.com/video/E/
+
+
+
+
+
+
+
+ http://example.com/video/F/
+
+
+
+
+
+
+
+ http://example.com/video/A/
+
+
+
+
+
+
+
+ http://example.com/video/B/
+
+
+
+
+
+
+
+ http://example.com/video/C/
+
+
+
+
+
+
+
+
+
+
+
+
+
+ http://example.com/audio/v0/
+
+
+
+
+
+
+
+
+
+ http://example.com/video/D/
+
+
+
+
+
+
+
+ http://example.com/video/E/
+
+
+
+
+
+
+
+ http://example.com/video/F/
+
+
+
+
+
+
+
+ http://example.com/video/A/
+
+
+
+
+
+
+
+ http://example.com/video/B/
+
+
+
+
+
+
+
+ http://example.com/video/C/
+
+
+
+
+
+
+
+
+
+
+
+
+
+ http://example.com/audio/v0/
+
+
+
+
+
+
+
+
+
+
+ http://example.com/video/D/
+
+
+
+
+
+
+
+ http://example.com/video/E/
+
+
+
+
+
+
+
+ http://example.com/video/F/
+
+
+
+
+
+
+
+
+
+ http://example.com/video/A/
+
+
+
+
+
+
+
+ http://example.com/video/B/
+
+
+
+
+
+
+
+ http://example.com/video/C/
+
+
+
+
+
+
+
+
+
+
diff --git a/test/manifests/multiperiod.js b/test/manifests/multiperiod.js
index f61feac7..3788ce72 100644
--- a/test/manifests/multiperiod.js
+++ b/test/manifests/multiperiod.js
@@ -270,6 +270,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -538,6 +539,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -824,6 +826,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -1098,6 +1101,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -1372,6 +1376,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
diff --git a/test/manifests/segmentBase.js b/test/manifests/segmentBase.js
index 1fb5d5d0..86a38e1d 100644
--- a/test/manifests/segmentBase.js
+++ b/test/manifests/segmentBase.js
@@ -27,11 +27,13 @@ export const parsedManifest = {
resolvedUri: '',
targetDuration: 6,
mediaSequence: 0,
+ discontinuitySequence: 0,
segments: [
{
duration: 6,
timeline: 0,
number: 0,
+ presentationTime: 0,
map: {
uri: '',
resolvedUri: 'https://www.example.com/1080p.ts'
diff --git a/test/manifests/segmentList.js b/test/manifests/segmentList.js
index 85f67552..ec9cab80 100644
--- a/test/manifests/segmentList.js
+++ b/test/manifests/segmentList.js
@@ -24,7 +24,8 @@ export const parsedManifest = {
'SUBTITLES': 'subs'
},
endList: true,
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
targetDuration: 1,
resolvedUri: '',
segments: [
@@ -38,7 +39,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 0,
uri: 'low/segment-1.ts',
- number: 1
+ number: 0
},
{
duration: 1,
@@ -50,7 +51,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 1,
uri: 'low/segment-2.ts',
- number: 2
+ number: 1
},
{
duration: 1,
@@ -62,7 +63,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 2,
uri: 'low/segment-3.ts',
- number: 3
+ number: 2
},
{
duration: 1,
@@ -74,7 +75,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 3,
uri: 'low/segment-4.ts',
- number: 4
+ number: 3
},
{
duration: 1,
@@ -86,7 +87,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 4,
uri: 'low/segment-5.ts',
- number: 5
+ number: 4
},
{
duration: 1,
@@ -98,7 +99,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 5,
uri: 'low/segment-6.ts',
- number: 6
+ number: 5
}
],
timeline: 0,
@@ -119,7 +120,8 @@ export const parsedManifest = {
},
endList: true,
resolvedUri: '',
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
targetDuration: 60,
segments: [
{
@@ -132,7 +134,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 0,
uri: 'high/segment-1.ts',
- number: 1
+ number: 0
},
{
duration: 60,
@@ -144,7 +146,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 60,
uri: 'high/segment-2.ts',
- number: 2
+ number: 1
},
{
duration: 60,
@@ -156,7 +158,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 120,
uri: 'high/segment-3.ts',
- number: 3
+ number: 2
},
{
duration: 60,
@@ -168,7 +170,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 180,
uri: 'high/segment-4.ts',
- number: 4
+ number: 3
},
{
duration: 60,
@@ -180,7 +182,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 240,
uri: 'high/segment-5.ts',
- number: 5
+ number: 4
},
{
duration: 60,
@@ -192,7 +194,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 300,
uri: 'high/segment-6.ts',
- number: 6
+ number: 5
},
{
duration: 60,
@@ -204,7 +206,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 360,
uri: 'high/segment-7.ts',
- number: 7
+ number: 6
},
{
duration: 60,
@@ -216,7 +218,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 420,
uri: 'high/segment-8.ts',
- number: 8
+ number: 7
},
{
duration: 60,
@@ -228,7 +230,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 480,
uri: 'high/segment-9.ts',
- number: 9
+ number: 8
},
{
duration: 60,
@@ -240,7 +242,7 @@ export const parsedManifest = {
timeline: 0,
presentationTime: 540,
uri: 'high/segment-10.ts',
- number: 10
+ number: 9
}
],
timeline: 0,
diff --git a/test/manifests/vtt_codecs.js b/test/manifests/vtt_codecs.js
index 937439aa..22864c07 100644
--- a/test/manifests/vtt_codecs.js
+++ b/test/manifests/vtt_codecs.js
@@ -74,6 +74,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -146,6 +147,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -226,6 +228,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -298,6 +301,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -339,10 +343,12 @@ export const parsedManifest = {
timeline: 0,
resolvedUri: 'https://example.com/en.dash',
duration: 6,
+ presentationTime: 0,
number: 0
}
],
- mediaSequence: 0
+ mediaSequence: 0,
+ discontinuitySequence: 0
}
],
uri: ''
@@ -369,10 +375,12 @@ export const parsedManifest = {
timeline: 0,
resolvedUri: 'https://example.com/es.vtt',
duration: 6,
+ presentationTime: 0,
number: 0
}
],
- mediaSequence: 0
+ mediaSequence: 0,
+ discontinuitySequence: 0
}
],
uri: ''
@@ -452,6 +460,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
@@ -530,6 +539,7 @@ export const parsedManifest = {
}
],
mediaSequence: 0,
+ discontinuitySequence: 0,
contentProtection: {
'com.widevine.alpha': {
attributes: {
diff --git a/test/manifests/webmsegments.js b/test/manifests/webmsegments.js
index bada5915..70728332 100644
--- a/test/manifests/webmsegments.js
+++ b/test/manifests/webmsegments.js
@@ -20,13 +20,13 @@ export const parsedManifest = {
},
uri: '',
endList: true,
- timeline: 1,
+ timeline: 0,
resolvedUri: '',
targetDuration: 4,
segments: [
{
uri: 'audio/segment_0.chk',
- timeline: 1,
+ timeline: 0,
duration: 4,
resolvedUri: 'https://www.example.com/audio/segment_0.chk',
map: {
@@ -38,7 +38,7 @@ export const parsedManifest = {
},
{
uri: 'audio/segment_1.chk',
- timeline: 1,
+ timeline: 0,
duration: 4,
resolvedUri: 'https://www.example.com/audio/segment_1.chk',
map: {
@@ -50,7 +50,7 @@ export const parsedManifest = {
},
{
uri: 'audio/segment_2.chk',
- timeline: 1,
+ timeline: 0,
duration: 4,
resolvedUri: 'https://www.example.com/audio/segment_2.chk',
map: {
@@ -62,7 +62,7 @@ export const parsedManifest = {
},
{
uri: 'audio/segment_3.chk',
- timeline: 1,
+ timeline: 0,
duration: 4,
resolvedUri: 'https://www.example.com/audio/segment_3.chk',
map: {
@@ -73,7 +73,8 @@ export const parsedManifest = {
presentationTime: 12
}
],
- mediaSequence: 0
+ mediaSequence: 0,
+ discontinuitySequence: 0
}
],
uri: ''
@@ -102,13 +103,13 @@ export const parsedManifest = {
},
uri: '',
endList: true,
- timeline: 1,
+ timeline: 0,
resolvedUri: '',
targetDuration: 4,
segments: [
{
uri: 'video/segment_0.chk',
- timeline: 1,
+ timeline: 0,
duration: 4,
resolvedUri: 'https://www.example.com/video/segment_0.chk',
map: {
@@ -120,7 +121,7 @@ export const parsedManifest = {
},
{
uri: 'video/segment_1.chk',
- timeline: 1,
+ timeline: 0,
duration: 4,
resolvedUri: 'https://www.example.com/video/segment_1.chk',
map: {
@@ -132,7 +133,7 @@ export const parsedManifest = {
},
{
uri: 'video/segment_2.chk',
- timeline: 1,
+ timeline: 0,
duration: 4,
resolvedUri: 'https://www.example.com/video/segment_2.chk',
map: {
@@ -144,7 +145,7 @@ export const parsedManifest = {
},
{
uri: 'video/segment_3.chk',
- timeline: 1,
+ timeline: 0,
duration: 4,
resolvedUri: 'https://www.example.com/video/segment_3.chk',
map: {
@@ -155,7 +156,8 @@ export const parsedManifest = {
presentationTime: 12
}
],
- mediaSequence: 0
+ mediaSequence: 0,
+ discontinuitySequence: 0
}
]
};
diff --git a/test/playlist-merge.test.js b/test/playlist-merge.test.js
new file mode 100644
index 00000000..6abfee28
--- /dev/null
+++ b/test/playlist-merge.test.js
@@ -0,0 +1,1938 @@
+import {
+ findPlaylistWithName,
+ findMediaGroupPlaylistWithName,
+ getRemovedPlaylists,
+ getRemovedMediaGroupPlaylists,
+ getIncompletePlaylists,
+ getMediaGroupPlaylists,
+ getMediaGroupPlaylistIdentificationObjects,
+ repositionSegmentsOnTimeline,
+ positionPlaylistOnTimeline,
+ positionPlaylistsOnTimeline,
+ positionMediaGroupPlaylistsOnTimeline,
+ removeMediaGroupPlaylists,
+ positionManifestOnTimeline
+} from '../src/playlist-merge';
+import QUnit from 'qunit';
+
+QUnit.module('findPlaylistWithName');
+
+QUnit.test('returns nothing when no playlists', function(assert) {
+ assert.notOk(findPlaylistWithName([], 'A'), 'nothing when no playlists');
+});
+
+QUnit.test('returns nothing when no match', function(assert) {
+ const playlists = [
+ { attributes: { NAME: 'B' } }
+ ];
+
+ assert.notOk(findPlaylistWithName(playlists, 'A'), 'nothing when no match');
+});
+
+QUnit.test('returns matching playlist', function(assert) {
+ const playlists = [
+ { attributes: { NAME: 'A' } },
+ { attributes: { NAME: 'B' } },
+ { attributes: { NAME: 'C' } }
+ ];
+
+ assert.deepEqual(
+ findPlaylistWithName(playlists, 'B'),
+ playlists[1],
+ 'returns matching playlist'
+ );
+});
+
+QUnit.module('findMediaGroupPlaylistWithName');
+
+QUnit.test('returns nothing when no media group playlists', function(assert) {
+ const manifest = { mediaGroups: { AUDIO: {} } };
+
+ assert.notOk(
+ findMediaGroupPlaylistWithName({
+ playlistName: 'A',
+ type: 'AUDIO',
+ group: 'audio',
+ label: 'en',
+ manifest
+ }),
+ 'returns nothing when no media group playlists'
+ );
+});
+
+QUnit.test('returns nothing when no match', function(assert) {
+ const manifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [ { attributes: { NAME: 'B' } } ]
+ }
+ }
+ }
+ }
+ };
+
+ assert.notOk(
+ findMediaGroupPlaylistWithName({
+ playlistName: 'A',
+ type: 'AUDIO',
+ group: 'audio',
+ label: 'en',
+ manifest
+ }),
+ 'returns nothing when no media group playlists'
+ );
+});
+
+QUnit.test('returns matching playlist', function(assert) {
+ const playlistB = { attributes: { NAME: 'B' } };
+ const manifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [
+ { attributes: { NAME: 'A' } },
+ { attributes: { NAME: 'B' } },
+ { attributes: { NAME: 'C' } }
+ ]
+ },
+ fr: {
+ playlists: [
+ { attributes: { NAME: 'A' } },
+ playlistB,
+ { attributes: { NAME: 'C' } }
+ ]
+ }
+ }
+ }
+ }
+ };
+
+ assert.deepEqual(
+ findMediaGroupPlaylistWithName({
+ playlistName: 'B',
+ type: 'AUDIO',
+ group: 'audio',
+ label: 'fr',
+ manifest
+ }),
+ playlistB,
+ 'returns matching playlist'
+ );
+});
+
+QUnit.module('getRemovedPlaylists');
+
+QUnit.test('returns nothing when no old playlists', function(assert) {
+ const newPlaylistA = { attributes: { NAME: 'A' } };
+ const newPlaylistB = { attributes: { NAME: 'B' } };
+ const newPlaylists = [newPlaylistA, newPlaylistB];
+
+ assert.deepEqual(
+ getRemovedPlaylists({ oldPlaylists: [], newPlaylists }),
+ [],
+ 'nothing when no old playlists'
+ );
+});
+
+QUnit.test('returns nothing when all playlists are available', function(assert) {
+ const oldPlaylistA = { attributes: { NAME: 'A' } };
+ const oldPlaylistB = { attributes: { NAME: 'B' } };
+ const newPlaylistA = { attributes: { NAME: 'A' } };
+ const newPlaylistB = { attributes: { NAME: 'B' } };
+ const oldPlaylists = [oldPlaylistA, oldPlaylistB];
+ const newPlaylists = [newPlaylistA, newPlaylistB];
+
+ assert.deepEqual(
+ getRemovedPlaylists({ oldPlaylists, newPlaylists }),
+ [],
+ 'nothing when all playlists are available'
+ );
+});
+
+QUnit.test('returns old playlists not available in new playlists', function(assert) {
+ const oldPlaylistA = { attributes: { NAME: 'A' } };
+ const oldPlaylistB = { attributes: { NAME: 'B' } };
+ const oldPlaylistC = { attributes: { NAME: 'C' } };
+ const oldPlaylistD = { attributes: { NAME: 'D' } };
+ const newPlaylistA = { attributes: { NAME: 'A' } };
+ const newPlaylistC = { attributes: { NAME: 'C' } };
+ const oldPlaylists = [oldPlaylistA, oldPlaylistB, oldPlaylistC, oldPlaylistD];
+ const newPlaylists = [newPlaylistA, newPlaylistC];
+
+ assert.deepEqual(
+ getRemovedPlaylists({ oldPlaylists, newPlaylists }),
+ [oldPlaylistB, oldPlaylistD],
+ 'returned old playlists not available in new playlists'
+ );
+});
+
+QUnit.module('getRemovedMediaGroupPlaylists');
+
+QUnit.test('returns nothing when no old media group playlists', function(assert) {
+ const playlistA = { attributes: { NAME: 'A' } };
+ const playlistB = { attributes: { NAME: 'B' } };
+ const oldManifest = { mediaGroups: { AUDIO: {} } };
+ const newManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [playlistA]
+ },
+ fr: {
+ playlists: [playlistB]
+ }
+ }
+ }
+ }
+ };
+
+ assert.deepEqual(
+ getRemovedMediaGroupPlaylists({ oldManifest, newManifest }),
+ [],
+ 'nothing when no old playlists'
+ );
+});
+
+QUnit.test('returns nothing when all media group playlists are available', function(assert) {
+ const oldPlaylistA = { attributes: { NAME: 'A' } };
+ const oldPlaylistB = { attributes: { NAME: 'B' } };
+ const newPlaylistA = { attributes: { NAME: 'A' } };
+ const newPlaylistB = { attributes: { NAME: 'B' } };
+ const oldManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [oldPlaylistA]
+ },
+ fr: {
+ playlists: [oldPlaylistB]
+ }
+ }
+ }
+ }
+ };
+ const newManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [newPlaylistA]
+ },
+ fr: {
+ playlists: [newPlaylistB]
+ }
+ }
+ }
+ }
+ };
+
+ assert.deepEqual(
+ getRemovedMediaGroupPlaylists({ oldManifest, newManifest }),
+ [],
+ 'nothing when all playlists are available'
+ );
+});
+
+QUnit.test('returns old media group playlists not available in new media group playlists', function(assert) {
+ const oldPlaylistA = { attributes: { NAME: 'A' } };
+ const oldPlaylistB = { attributes: { NAME: 'B' } };
+ const oldPlaylistC = { attributes: { NAME: 'C' } };
+ const oldPlaylistD = { attributes: { NAME: 'D' } };
+ const newPlaylistA = { attributes: { NAME: 'A' } };
+ const newPlaylistC = { attributes: { NAME: 'C' } };
+ const oldManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [oldPlaylistA, oldPlaylistB]
+ },
+ fr: {
+ playlists: [oldPlaylistC, oldPlaylistD]
+ }
+ }
+ }
+ }
+ };
+ const newManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [newPlaylistA]
+ },
+ fr: {
+ playlists: [newPlaylistC]
+ }
+ }
+ }
+ }
+ };
+
+ assert.deepEqual(
+ getRemovedMediaGroupPlaylists({ oldManifest, newManifest }),
+ [{
+ type: 'AUDIO',
+ group: 'audio',
+ label: 'en',
+ playlist: oldPlaylistB
+ }, {
+ type: 'AUDIO',
+ group: 'audio',
+ label: 'fr',
+ playlist: oldPlaylistD
+ }],
+ 'returned old playlists not available in new playlists'
+ );
+});
+
+QUnit.module('getIncompletePlaylists');
+
+QUnit.test('returns nothing when no playlists', function(assert) {
+ assert.deepEqual(getIncompletePlaylists([]), [], 'nothing when no playlists');
+});
+
+QUnit.test('returns nothing when no incomplete playlists', function(assert) {
+ const playlistA = {
+ attributes: { NAME: 'A' },
+ segments: [{ timeline: 0 }, { timeline: 1 }, { timeline: 2 }]
+ };
+ const playlistB = {
+ attributes: { NAME: 'B' },
+ segments: [{ timeline: 0 }, { timeline: 1 }, { timeline: 2 }]
+ };
+ const playlistC = {
+ attributes: { NAME: 'C' },
+ segments: [{ timeline: 0 }, { timeline: 1 }, { timeline: 2 }]
+ };
+
+ assert.deepEqual(
+ getIncompletePlaylists([playlistA, playlistB, playlistC]),
+ [],
+ 'nothing when no incomplete playlists'
+ );
+});
+
+QUnit.test('returns playlists that don\'t account for all timelines', function(assert) {
+ const playlistA = {
+ attributes: { NAME: 'A' },
+ segments: [{ timeline: 0 }, { timeline: 1 }]
+ };
+ const playlistB = {
+ attributes: { NAME: 'B' },
+ segments: [{ timeline: 0 }, { timeline: 1 }, { timeline: 2 }]
+ };
+ const playlistC = {
+ attributes: { NAME: 'C' },
+ segments: [{ timeline: 0 }, { timeline: 1 }, { timeline: 2 }]
+ };
+ const playlistD = {
+ attributes: { NAME: 'D' },
+ segments: [{ timeline: 0 }, { timeline: 1 }]
+ };
+
+ assert.deepEqual(
+ getIncompletePlaylists([playlistA, playlistB, playlistC, playlistD]),
+ [playlistA, playlistD],
+ 'returns incomplete playlists'
+ );
+});
+
+QUnit.module('getMediaGroupPlaylists');
+
+QUnit.test('returns nothing when no media group playlists', function(assert) {
+ const manifest = {
+ mediaGroups: {
+ AUDIO: {}
+ }
+ };
+
+ assert.deepEqual(
+ getMediaGroupPlaylists(manifest),
+ [],
+ 'nothing when no media group playlists'
+ );
+});
+
+QUnit.test('returns media group playlists', function(assert) {
+ const playlistEnA = { attributes: { NAME: 'A' } };
+ const playlistEnB = { attributes: { NAME: 'B' } };
+ const playlistEnC = { attributes: { NAME: 'C' } };
+ const playlistFrA = { attributes: { NAME: 'A' } };
+ const playlistFrB = { attributes: { NAME: 'B' } };
+ const manifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [playlistEnA, playlistEnB, playlistEnC]
+ },
+ fr: {
+ playlists: [playlistFrA, playlistFrB]
+ }
+ }
+ }
+ }
+ };
+
+ assert.deepEqual(
+ getMediaGroupPlaylists(manifest),
+ [playlistEnA, playlistEnB, playlistEnC, playlistFrA, playlistFrB],
+ 'returns media group playlists'
+ );
+});
+
+QUnit.module('getMediaGroupPlaylistIdentificationObjects');
+
+QUnit.test('returns nothing when no playlists', function(assert) {
+ const manifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [{ attributes: { NAME: 'A' } }]
+ },
+ fr: {
+ playlists: [{ attributes: { NAME: 'A' } }]
+ }
+ }
+ }
+ }
+ };
+
+ assert.deepEqual(
+ getMediaGroupPlaylistIdentificationObjects({ playlists: [], manifest }),
+ [],
+ 'nothing when no playlists passed in'
+ );
+});
+
+QUnit.test('returns ID objects for passed playlists', function(assert) {
+ const playlistEnA = { attributes: { NAME: 'A' } };
+ const playlistEnB = { attributes: { NAME: 'B' } };
+ const playlistEnC = { attributes: { NAME: 'C' } };
+ const playlistFrA = { attributes: { NAME: 'A' } };
+ const playlistFrB = { attributes: { NAME: 'B' } };
+ const manifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [playlistEnA, playlistEnB, playlistEnC]
+ },
+ fr: {
+ playlists: [playlistFrA, playlistFrB]
+ }
+ }
+ }
+ }
+ };
+
+ assert.deepEqual(
+ getMediaGroupPlaylistIdentificationObjects({
+ playlists: [playlistEnB, playlistFrA],
+ manifest
+ }),
+ [{
+ type: 'AUDIO',
+ group: 'audio',
+ label: 'en',
+ playlist: playlistEnB
+ }, {
+ type: 'AUDIO',
+ group: 'audio',
+ label: 'fr',
+ playlist: playlistFrA
+ }],
+ 'returns ID objects for passed playlists'
+ );
+});
+
+QUnit.module('repositionSegmentsOnTimeline');
+
+QUnit.test('updates segment timelines and mediaSequence numbers', function(assert) {
+ const segments = [{
+ // first segment discontinuity case, discontinuity should not increment timelineStart
+ discontinuity: true,
+ number: 0,
+ timeline: 0
+ }, {
+ number: 1,
+ timeline: 0
+ }, {
+ discontinuity: true,
+ number: 2,
+ timeline: 1
+ }, {
+ number: 3,
+ timeline: 1
+ }];
+
+ repositionSegmentsOnTimeline({ segments, mediaSequenceStart: 11, timelineStart: 21 });
+
+ assert.deepEqual(
+ segments,
+ [
+ {
+ discontinuity: true,
+ number: 11,
+ timeline: 21
+ },
+ {
+ number: 12,
+ timeline: 21
+ },
+ {
+ discontinuity: true,
+ number: 13,
+ timeline: 22
+ },
+ {
+ number: 14,
+ timeline: 22
+ }
+ ],
+ 'updated segment timelines and mediaSequence numbers'
+ );
+});
+
+QUnit.module('positionPlaylistOnTimeline');
+
+QUnit.test('correctly positions when no old segments', function(assert) {
+ const oldPlaylist = {
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ timeline: 2,
+ segments: []
+ };
+ const newPlaylist = {
+ // newly parsed playlists will appear as if starting from 0
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 0
+ }, {
+ number: 1,
+ timeline: 0
+ }, {
+ discontinuity: true,
+ number: 2,
+ timeline: 1
+ }, {
+ number: 3,
+ timeline: 1
+ }]
+ };
+
+ positionPlaylistOnTimeline(oldPlaylist, newPlaylist);
+
+ assert.deepEqual(
+ newPlaylist,
+ {
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ // timeline jumped because first segment discontinuity added
+ timeline: 3,
+ // discontinuityStarts added
+ discontinuityStarts: [0, 2],
+ segments: [{
+ // discontinuity is added
+ discontinuity: true,
+ number: 10,
+ timeline: 3
+ }, {
+ number: 11,
+ timeline: 3
+ }, {
+ discontinuity: true,
+ number: 12,
+ timeline: 4
+ }, {
+ number: 13,
+ timeline: 4
+ }]
+ },
+ 'correctly positioned new playlist when no old segments'
+ );
+});
+
+QUnit.test('correctly positions when no new segments', function(assert) {
+ const oldPlaylist = {
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [0, 2],
+ segments: [{
+ discontinuity: true,
+ number: 10,
+ timeline: 3
+ }, {
+ number: 11,
+ timeline: 3
+ }, {
+ discontinuity: true,
+ number: 12,
+ timeline: 4
+ }, {
+ number: 13,
+ timeline: 4
+ }]
+ };
+ const newPlaylist = {
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+
+ positionPlaylistOnTimeline(oldPlaylist, newPlaylist);
+
+ assert.deepEqual(
+ newPlaylist,
+ {
+ // last segment + 1
+ mediaSequence: 14,
+ // account for two removed discontinuities
+ discontinuitySequence: 4,
+ // when segments are added the timeline will jump, but stick with the last seen
+ // timeline
+ timeline: 4,
+ // discontinuityStarts added
+ discontinuityStarts: [],
+ segments: []
+ },
+ 'correctly positioned new playlist when no new segments'
+ );
+});
+
+QUnit.test('correctly positions when no old or new segments', function(assert) {
+ const oldPlaylist = {
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [],
+ segments: []
+ };
+ const newPlaylist = {
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+
+ positionPlaylistOnTimeline(oldPlaylist, newPlaylist);
+
+ assert.deepEqual(
+ newPlaylist,
+ {
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ timeline: 3,
+ // discontinuityStarts added
+ discontinuityStarts: [],
+ segments: []
+ },
+ 'correctly positioned new playlist when no old or new segments'
+ );
+});
+
+QUnit.test('correctly positions for complete refresh', function(assert) {
+ const oldPlaylist = {
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [0, 2],
+ segments: [{
+ discontinuity: true,
+ number: 10,
+ timeline: 3,
+ presentationTime: 100
+ }, {
+ number: 11,
+ timeline: 3,
+ presentationTime: 102
+ }, {
+ discontinuity: true,
+ number: 12,
+ timeline: 4,
+ presentationTime: 104
+ }, {
+ number: 13,
+ timeline: 4,
+ presentationTime: 106
+ }]
+ };
+ const newPlaylist = {
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 0,
+ // This doesn't match any old segments and represents a gap of 2 seconds (the
+ // segment with presentationTime of 108 was missed). This helps test that the gap is
+ // ignored and this segment continues on as if it were the next to be played in the
+ // stream.
+ presentationTime: 110
+ }, {
+ number: 1,
+ timeline: 0,
+ presentationTime: 112
+ }, {
+ discontinuity: true,
+ number: 2,
+ timeline: 1,
+ presentationTime: 114
+ }]
+ };
+
+ positionPlaylistOnTimeline(oldPlaylist, newPlaylist);
+
+ assert.deepEqual(
+ newPlaylist,
+ {
+ // last segment + 1
+ mediaSequence: 14,
+ // account for two removed discontinuities
+ discontinuitySequence: 4,
+ // increase the last seen timeline by one since these new segments are considered
+ // discontinuous with the prior ones
+ timeline: 5,
+ // discontinuityStarts added
+ discontinuityStarts: [0, 2],
+ segments: [{
+ discontinuity: true,
+ number: 14,
+ timeline: 5,
+ presentationTime: 110
+ }, {
+ number: 15,
+ timeline: 5,
+ presentationTime: 112
+ }, {
+ discontinuity: true,
+ number: 16,
+ timeline: 6,
+ presentationTime: 114
+ }]
+ },
+ 'correctly positioned new playlist when manifest completely refreshed'
+ );
+});
+
+QUnit.test('correctly positions when matching segment', function(assert) {
+ const oldPlaylist = {
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [0, 2],
+ segments: [{
+ discontinuity: true,
+ number: 10,
+ timeline: 3,
+ presentationTime: 100
+ }, {
+ number: 11,
+ timeline: 3,
+ presentationTime: 102
+ }, {
+ number: 12,
+ timeline: 4,
+ presentationTime: 104
+ }, {
+ number: 13,
+ timeline: 4,
+ presentationTime: 106
+ }]
+ };
+ const newPlaylist = {
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 0,
+ // matches second to last segment
+ presentationTime: 104
+ }, {
+ number: 1,
+ timeline: 0,
+ presentationTime: 106
+ }, {
+ discontinuity: true,
+ number: 2,
+ timeline: 1,
+ presentationTime: 108
+ }]
+ };
+
+ positionPlaylistOnTimeline(oldPlaylist, newPlaylist);
+
+ assert.deepEqual(
+ newPlaylist,
+ {
+ // matching segment's media sequence
+ mediaSequence: 12,
+ // account for one removed discontinuity
+ discontinuitySequence: 3,
+ // matching segment's timeline
+ timeline: 4,
+ discontinuityStarts: [2],
+ segments: [{
+ number: 12,
+ timeline: 4,
+ presentationTime: 104
+ }, {
+ number: 13,
+ timeline: 4,
+ presentationTime: 106
+ }, {
+ discontinuity: true,
+ number: 14,
+ timeline: 5,
+ presentationTime: 108
+ }]
+ },
+ 'correctly positioned new playlist by segment match'
+ );
+});
+
+QUnit.test('handles matching segment with removed segments from end', function(assert) {
+ const oldPlaylist = {
+ mediaSequence: 12,
+ discontinuitySequence: 3,
+ timeline: 4,
+ discontinuityStarts: [],
+ segments: [{
+ number: 12,
+ timeline: 4,
+ presentationTime: 104
+ }, {
+ number: 13,
+ timeline: 4,
+ presentationTime: 106
+ }, {
+ number: 14,
+ timeline: 4,
+ presentationTime: 108
+ }, {
+ number: 15,
+ timeline: 4,
+ presentationTime: 110
+ }]
+ };
+ const newPlaylist = {
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 0,
+ presentationTime: 104
+ }, {
+ number: 1,
+ timeline: 0,
+ presentationTime: 106
+ }]
+ // two segments missing that were in prior playlist
+ };
+
+ positionPlaylistOnTimeline(oldPlaylist, newPlaylist);
+
+ assert.deepEqual(
+ newPlaylist,
+ {
+ mediaSequence: 12,
+ discontinuitySequence: 3,
+ timeline: 4,
+ discontinuityStarts: [],
+ segments: [{
+ number: 12,
+ timeline: 4,
+ presentationTime: 104
+ }, {
+ number: 13,
+ timeline: 4,
+ presentationTime: 106
+ }]
+ },
+ 'correctly positioned new playlist by segment match with removed segments'
+ );
+});
+
+QUnit.module('positionPlaylistsOnTimeline');
+
+QUnit.test('positions multiple playlists', function(assert) {
+ const oldPlaylistA = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ timeline: 3,
+ segments: []
+ };
+ const oldPlaylistB = {
+ attributes: { NAME: 'B' },
+ mediaSequence: 9,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [],
+ segments: [{
+ number: 9,
+ timeline: 3,
+ presentationTime: 102
+ }]
+ };
+ // from empty to one segment
+ const newPlaylistA = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 1,
+ presentationTime: 102
+ }]
+ };
+ // from one segment to empty
+ const newPlaylistB = {
+ attributes: { NAME: 'B' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ const oldPlaylists = [oldPlaylistA, oldPlaylistB];
+ const newPlaylists = [newPlaylistA, newPlaylistB];
+
+ positionPlaylistsOnTimeline({ oldPlaylists, newPlaylists });
+
+ assert.deepEqual(
+ newPlaylists,
+ [
+ {
+ attributes: { NAME: 'A' },
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ discontinuityStarts: [0],
+ // increased timeline
+ timeline: 4,
+ segments: [{
+ // added discontinuity
+ discontinuity: true,
+ number: 10,
+ timeline: 4,
+ presentationTime: 102
+ }]
+ },
+ {
+ attributes: { NAME: 'B' },
+ // increased mediaSequence to account for segment that fell off
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ discontinuityStarts: [],
+ // same timeline as old
+ timeline: 3,
+ segments: []
+ }
+ ],
+ 'correctly positioned multiple playlists'
+ );
+});
+
+QUnit.module('positionMediaGroupPlaylistsOnTimeline');
+
+QUnit.test('positions multiple media group playlists', function(assert) {
+ const oldPlaylistA = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ timeline: 3,
+ segments: []
+ };
+ const oldPlaylistB = {
+ attributes: { NAME: 'B' },
+ mediaSequence: 9,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [],
+ segments: [{
+ number: 9,
+ timeline: 3,
+ presentationTime: 102
+ }]
+ };
+ const oldPlaylistC = {
+ attributes: { NAME: 'C' },
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ timeline: 3,
+ segments: []
+ };
+ const oldPlaylistD = {
+ attributes: { NAME: 'D' },
+ mediaSequence: 9,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [],
+ segments: [{
+ number: 9,
+ timeline: 3,
+ presentationTime: 102
+ }]
+ };
+ // from empty to one segment
+ const newPlaylistA = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 1,
+ presentationTime: 102
+ }]
+ };
+ // from one segment to empty
+ const newPlaylistB = {
+ attributes: { NAME: 'B' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ // from empty to one segment
+ const newPlaylistC = {
+ attributes: { NAME: 'C' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 1,
+ presentationTime: 102
+ }]
+ };
+ // from one segment to empty
+ const newPlaylistD = {
+ attributes: { NAME: 'D' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ const oldManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ // en and es have the same cases, but different playlist names to ensure that
+ // all media group labels are checked and updated
+ en: {
+ playlists: [oldPlaylistA, oldPlaylistB]
+ },
+ es: {
+ playlists: [oldPlaylistC, oldPlaylistD]
+ }
+ }
+ }
+ }
+ };
+ const newManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [newPlaylistA, newPlaylistB]
+ },
+ es: {
+ playlists: [newPlaylistC, newPlaylistD]
+ }
+ }
+ }
+ }
+ };
+
+ positionMediaGroupPlaylistsOnTimeline({ oldManifest, newManifest });
+
+ assert.deepEqual(
+ newManifest,
+ {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [
+ {
+ attributes: { NAME: 'A' },
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ discontinuityStarts: [0],
+ // increased timeline
+ timeline: 4,
+ segments: [{
+ // added discontinuity
+ discontinuity: true,
+ number: 10,
+ timeline: 4,
+ presentationTime: 102
+ }]
+ },
+ {
+ attributes: { NAME: 'B' },
+ // increased mediaSequence to account for segment that fell off
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ discontinuityStarts: [],
+ // same timeline as old
+ timeline: 3,
+ segments: []
+ }
+ ]
+ },
+ es: {
+ playlists: [
+ {
+ attributes: { NAME: 'C' },
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ discontinuityStarts: [0],
+ // increased timeline
+ timeline: 4,
+ segments: [{
+ // added discontinuity
+ discontinuity: true,
+ number: 10,
+ timeline: 4,
+ presentationTime: 102
+ }]
+ },
+ {
+ attributes: { NAME: 'D' },
+ // increased mediaSequence to account for segment that fell off
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ discontinuityStarts: [],
+ // same timeline as old
+ timeline: 3,
+ segments: []
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
+ 'correctly positioned multiple media group playlists'
+ );
+});
+
+QUnit.module('removeMediaGroupPlaylists');
+
+QUnit.test('no change if no playlists', function(assert) {
+ const playlist = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ const manifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [playlist]
+ }
+ }
+ }
+ }
+ };
+
+ removeMediaGroupPlaylists({ manifest, playlists: [] });
+
+ assert.deepEqual(
+ manifest,
+ {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [playlist]
+ }
+ }
+ }
+ }
+ },
+ 'no chage when no playlists'
+ );
+});
+
+QUnit.test('removes playlists from media group', function(assert) {
+ const playlistA = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ const playlistB = {
+ attributes: { NAME: 'B' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ const playlistC = {
+ attributes: { NAME: 'C' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ const manifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [playlistA, playlistB, playlistC]
+ }
+ }
+ }
+ }
+ };
+
+ removeMediaGroupPlaylists({ manifest, playlists: [playlistA, playlistC] });
+
+ assert.deepEqual(
+ manifest,
+ {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [playlistB]
+ }
+ }
+ }
+ }
+ },
+ 'removed playlists'
+ );
+});
+
+QUnit.test('removes media group if no playlists after removal', function(assert) {
+ const playlistA = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ const playlistB = {
+ attributes: { NAME: 'B' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ const manifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [playlistA, playlistB]
+ }
+ }
+ }
+ }
+ };
+
+ removeMediaGroupPlaylists({ manifest, playlists: [playlistA, playlistB] });
+
+ assert.deepEqual(
+ manifest,
+ { mediaGroups: { AUDIO: {} } },
+ 'removed playlists and group'
+ );
+});
+
+QUnit.test('leaves other media groups when removing one', function(assert) {
+ const playlistA = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ const playlistB = {
+ attributes: { NAME: 'B' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ const playlistC = {
+ attributes: { NAME: 'C' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ const manifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [playlistA, playlistB]
+ },
+ es: {
+ playlists: [playlistC]
+ }
+ }
+ }
+ }
+ };
+
+ removeMediaGroupPlaylists({ manifest, playlists: [playlistA, playlistB] });
+
+ assert.deepEqual(
+ manifest,
+ {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ es: {
+ playlists: [playlistC]
+ }
+ }
+ }
+ }
+ },
+ 'removed playlist and label, left other label alone'
+ );
+});
+
+QUnit.module('positionManifestOnTimeline');
+
+QUnit.test('returns manifest unchanged if no new playlists', function(assert) {
+ const playlist = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ timeline: 3,
+ segments: []
+ };
+ const oldManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: []
+ }
+ }
+ }
+ },
+ playlists: [playlist]
+ };
+ const newManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: []
+ }
+ }
+ }
+ },
+ playlists: []
+ };
+
+ assert.deepEqual(
+ positionManifestOnTimeline({ oldManifest, newManifest }),
+ {
+ // should exclude the old playlist
+ playlistsToExclude: [playlist],
+ mediaGroupPlaylistsToExclude: [],
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: []
+ }
+ }
+ }
+ },
+ playlists: []
+ },
+ 'returns manifest unchanged if no playlists'
+ );
+});
+
+QUnit.test('returns manifest unchanged if no old playlists', function(assert) {
+ const playlist = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ const oldManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: []
+ }
+ }
+ }
+ },
+ playlists: []
+ };
+ const newManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: []
+ }
+ }
+ }
+ },
+ playlists: [playlist]
+ };
+
+ assert.deepEqual(
+ positionManifestOnTimeline({ oldManifest, newManifest }),
+ {
+ playlistsToExclude: [],
+ mediaGroupPlaylistsToExclude: [],
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: []
+ }
+ }
+ }
+ },
+ playlists: [playlist]
+ },
+ 'returns manifest unchanged if no playlists'
+ );
+});
+
+QUnit.test('returns updated manifest and playlists to exclude', function(assert) {
+ const oldPlaylistA = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 9,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [],
+ segments: [{
+ number: 9,
+ timeline: 3,
+ presentationTime: 102
+ }]
+ };
+ const oldPlaylistB = {
+ attributes: { NAME: 'B' },
+ mediaSequence: 9,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [],
+ segments: [{
+ number: 9,
+ timeline: 3,
+ presentationTime: 102
+ }]
+ };
+ const oldPlaylistC = {
+ attributes: { NAME: 'C' },
+ mediaSequence: 9,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [],
+ segments: [{
+ number: 9,
+ timeline: 3,
+ presentationTime: 102
+ }]
+ };
+ const oldPlaylistD = {
+ attributes: { NAME: 'D' },
+ mediaSequence: 9,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [],
+ segments: [{
+ number: 9,
+ timeline: 3,
+ presentationTime: 102
+ }]
+ };
+ const oldPlaylistE = {
+ attributes: { NAME: 'E' },
+ mediaSequence: 9,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [],
+ segments: [{
+ number: 9,
+ timeline: 3,
+ presentationTime: 102
+ }]
+ };
+ const oldPlaylistF = {
+ attributes: { NAME: 'F' },
+ mediaSequence: 9,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [],
+ segments: [{
+ number: 9,
+ timeline: 3,
+ presentationTime: 102
+ }]
+ };
+ // matches the first segment from prior
+ const newPlaylistA = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 0,
+ presentationTime: 102
+ }, {
+ discontinuity: true,
+ number: 1,
+ timeline: 1,
+ presentationTime: 104
+ }]
+ };
+ // matches the first segment from prior
+ const newPlaylistB = {
+ attributes: { NAME: 'B' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 0,
+ presentationTime: 102
+ }, {
+ discontinuity: true,
+ number: 1,
+ timeline: 1,
+ presentationTime: 104
+ }]
+ };
+ // incomplete playlist (missing second timeline)
+ const newPlaylistE = {
+ attributes: { NAME: 'E' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 0,
+ presentationTime: 102
+ }]
+ };
+ // incomplete playlist (missing second timeline)
+ const newPlaylistF = {
+ attributes: { NAME: 'F' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 0,
+ presentationTime: 102
+ }]
+ };
+ const oldManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [oldPlaylistB, oldPlaylistD, oldPlaylistF]
+ }
+ }
+ }
+ },
+ playlists: [oldPlaylistA, oldPlaylistC, oldPlaylistE]
+ };
+ const newManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ // removed playlist D, playlist F incomplete
+ playlists: [newPlaylistB, newPlaylistF]
+ }
+ }
+ }
+ },
+ // removed playlist C, playlist E incomplete
+ playlists: [newPlaylistA, newPlaylistE]
+ };
+
+ assert.deepEqual(
+ positionManifestOnTimeline({ oldManifest, newManifest }),
+ {
+ playlistsToExclude: [oldPlaylistC, newPlaylistE],
+ mediaGroupPlaylistsToExclude: [{
+ group: 'audio',
+ label: 'en',
+ type: 'AUDIO',
+ playlist: oldPlaylistD
+ }, {
+ group: 'audio',
+ label: 'en',
+ type: 'AUDIO',
+ playlist: newPlaylistF
+ }],
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ // no playlists D or F
+ playlists: [{
+ attributes: { NAME: 'B' },
+ // increased mediaSequence to account for segment that fell off
+ mediaSequence: 9,
+ discontinuitySequence: 2,
+ discontinuityStarts: [1],
+ // same timeline as old
+ timeline: 3,
+ segments: [{
+ number: 9,
+ timeline: 3,
+ presentationTime: 102
+ }, {
+ discontinuity: true,
+ number: 10,
+ timeline: 4,
+ presentationTime: 104
+ }]
+ }]
+ }
+ }
+ }
+ },
+ // no playlists C or E
+ playlists: [{
+ attributes: { NAME: 'A' },
+ mediaSequence: 9,
+ discontinuitySequence: 2,
+ discontinuityStarts: [1],
+ // increased timeline
+ timeline: 3,
+ segments: [{
+ number: 9,
+ timeline: 3,
+ presentationTime: 102
+ }, {
+ discontinuity: true,
+ number: 10,
+ timeline: 4,
+ presentationTime: 104
+ }]
+ }]
+ },
+ 'returns excluded playlist and updated manifest, removed incomplete playlist'
+ );
+});
+
+QUnit.test('returns updated audio only manifest and playlists to exclude', function(assert) {
+ const oldPlaylistB = {
+ attributes: { NAME: 'B' },
+ mediaSequence: 9,
+ discontinuitySequence: 2,
+ timeline: 3,
+ discontinuityStarts: [],
+ segments: [{
+ number: 9,
+ timeline: 3,
+ presentationTime: 102
+ }]
+ };
+ const oldPlaylistC = {
+ attributes: { NAME: 'C' },
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ timeline: 3,
+ segments: []
+ };
+ // from one segment to empty
+ const newPlaylistB = {
+ attributes: { NAME: 'B' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: []
+ };
+ const oldManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [oldPlaylistB, oldPlaylistC]
+ }
+ }
+ }
+ },
+ playlists: []
+ };
+ const newManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ // removed playlist C
+ playlists: [newPlaylistB]
+ }
+ }
+ }
+ },
+ playlists: []
+ };
+
+ assert.deepEqual(
+ positionManifestOnTimeline({ oldManifest, newManifest }),
+ {
+ playlistsToExclude: [],
+ mediaGroupPlaylistsToExclude: [{
+ group: 'audio',
+ label: 'en',
+ type: 'AUDIO',
+ playlist: oldPlaylistC
+ }],
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [{
+ attributes: { NAME: 'B' },
+ // increased mediaSequence to account for segment that fell off
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ discontinuityStarts: [],
+ // same timeline as old
+ timeline: 3,
+ segments: []
+ }]
+ }
+ }
+ }
+ },
+ playlists: []
+ },
+ 'returns excluded playlist and updated audio only manifest'
+ );
+});
+
+// In the future, there should be logic to handle the addition of playlists, but for now,
+// the test exists to ensure nothing breaks before that logic is added.
+QUnit.test('exludes playlists not seen before', function(assert) {
+ const oldPlaylistA = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ timeline: 3,
+ segments: []
+ };
+ // from empty to two segments
+ const newPlaylistA = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 1,
+ presentationTime: 102
+ }, {
+ discontinuity: true,
+ number: 1,
+ timeline: 2,
+ presentationTime: 104
+ }]
+ };
+ // new playlist
+ const newPlaylistB = {
+ attributes: { NAME: 'B' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 1,
+ presentationTime: 102
+ }, {
+ discontinuity: true,
+ number: 1,
+ timeline: 2,
+ presentationTime: 104
+ }]
+ };
+ const oldManifest = {
+ mediaGroups: {},
+ playlists: [oldPlaylistA]
+ };
+ const newManifest = {
+ mediaGroups: {},
+ // added playlist B
+ playlists: [newPlaylistA, newPlaylistB]
+ };
+
+ assert.deepEqual(
+ positionManifestOnTimeline({ oldManifest, newManifest }),
+ {
+ playlistsToExclude: [],
+ mediaGroupPlaylistsToExclude: [],
+ mediaGroups: {},
+ // no playlist B
+ playlists: [{
+ attributes: { NAME: 'A' },
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ discontinuityStarts: [0, 1],
+ // increased timeline
+ timeline: 4,
+ segments: [{
+ // added discontinuity
+ discontinuity: true,
+ number: 10,
+ timeline: 4,
+ presentationTime: 102
+ }, {
+ discontinuity: true,
+ number: 11,
+ timeline: 5,
+ presentationTime: 104
+ }]
+ }]
+ },
+ 'returns playlist and updated manifest, removed new playlist'
+ );
+});
+
+// In the future, there should be logic to handle the addition of media group playlists,
+// but for now, the test exists to ensure nothing breaks before that logic is added.
+QUnit.test('does not include media group playlists not seen before', function(assert) {
+ const oldPlaylistA = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ timeline: 3,
+ segments: []
+ };
+ // from empty to two segments
+ const newPlaylistA = {
+ attributes: { NAME: 'A' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 1,
+ presentationTime: 102
+ }, {
+ discontinuity: true,
+ number: 1,
+ timeline: 2,
+ presentationTime: 104
+ }]
+ };
+ // new playlist
+ const newPlaylistB = {
+ attributes: { NAME: 'B' },
+ mediaSequence: 0,
+ discontinuitySequence: 0,
+ timeline: 0,
+ segments: [{
+ number: 0,
+ timeline: 1,
+ presentationTime: 102
+ }, {
+ discontinuity: true,
+ number: 1,
+ timeline: 2,
+ presentationTime: 104
+ }]
+ };
+ const oldManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ playlists: [oldPlaylistA]
+ }
+ }
+ }
+ },
+ playlists: []
+ };
+ const newManifest = {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ // added playlist B
+ playlists: [newPlaylistA, newPlaylistB]
+ }
+ }
+ }
+ },
+ playlists: []
+ };
+
+ assert.deepEqual(
+ positionManifestOnTimeline({ oldManifest, newManifest }),
+ {
+ mediaGroups: {
+ AUDIO: {
+ audio: {
+ en: {
+ // no playlist B
+ playlists: [{
+ attributes: { NAME: 'A' },
+ mediaSequence: 10,
+ discontinuitySequence: 2,
+ discontinuityStarts: [0, 1],
+ // increased timeline
+ timeline: 4,
+ segments: [{
+ // added discontinuity
+ discontinuity: true,
+ number: 10,
+ timeline: 4,
+ presentationTime: 102
+ }, {
+ discontinuity: true,
+ number: 11,
+ timeline: 5,
+ presentationTime: 104
+ }]
+ }]
+ }
+ }
+ }
+ },
+ playlists: [],
+ playlistsToExclude: [],
+ mediaGroupPlaylistsToExclude: []
+ },
+ 'returns updated manifest, removed new media group playlist'
+ );
+});
diff --git a/test/segment/segmentBase.test.js b/test/segment/segmentBase.test.js
index 974354d6..bf8dada6 100644
--- a/test/segment/segmentBase.test.js
+++ b/test/segment/segmentBase.test.js
@@ -11,6 +11,7 @@ QUnit.test('sets segment to baseUrl', function(assert) {
const inputAttributes = {
baseUrl: 'http://www.example.com/i.fmp4',
initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
+ periodStart: 0,
type: 'static'
};
@@ -21,6 +22,7 @@ QUnit.test('sets segment to baseUrl', function(assert) {
},
resolvedUri: 'http://www.example.com/i.fmp4',
uri: 'http://www.example.com/i.fmp4',
+ presentationTime: 0,
number: 0
}]);
});
@@ -30,6 +32,7 @@ QUnit.test('sets duration based on sourceDuration', function(assert) {
baseUrl: 'http://www.example.com/i.fmp4',
initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
sourceDuration: 10,
+ periodStart: 0,
type: 'static'
};
@@ -42,6 +45,7 @@ QUnit.test('sets duration based on sourceDuration', function(assert) {
},
resolvedUri: 'http://www.example.com/i.fmp4',
uri: 'http://www.example.com/i.fmp4',
+ presentationTime: 0,
number: 0
}]);
});
@@ -59,6 +63,7 @@ QUnit.test('sets duration based on sourceDuration and not @timescale', function(
initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
sourceDuration: 10,
timescale: 2,
+ periodStart: 0,
type: 'static'
};
@@ -71,6 +76,7 @@ QUnit.test('sets duration based on sourceDuration and not @timescale', function(
},
resolvedUri: 'http://www.example.com/i.fmp4',
uri: 'http://www.example.com/i.fmp4',
+ presentationTime: 0,
number: 0
}]);
});
@@ -82,6 +88,7 @@ QUnit.test('sets duration based on @duration', function(assert) {
baseUrl: 'http://www.example.com/i.fmp4',
initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
periodIndex: 0,
+ periodStart: 0,
type: 'static'
};
@@ -94,6 +101,7 @@ QUnit.test('sets duration based on @duration', function(assert) {
},
resolvedUri: 'http://www.example.com/i.fmp4',
uri: 'http://www.example.com/i.fmp4',
+ presentationTime: 0,
number: 0
}]);
});
@@ -106,6 +114,7 @@ QUnit.test('sets duration based on @duration and @timescale', function(assert) {
baseUrl: 'http://www.example.com/i.fmp4',
initialization: { sourceURL: 'http://www.example.com/init.fmp4' },
periodIndex: 0,
+ periodStart: 0,
type: 'static'
};
@@ -118,6 +127,7 @@ QUnit.test('sets duration based on @duration and @timescale', function(assert) {
},
resolvedUri: 'http://www.example.com/i.fmp4',
uri: 'http://www.example.com/i.fmp4',
+ presentationTime: 0,
number: 0
}]);
});
@@ -133,6 +143,7 @@ QUnit.test('translates ranges in node', function(assert) {
range: '121-125'
},
periodIndex: 0,
+ periodStart: 0,
type: 'static'
};
@@ -149,6 +160,7 @@ QUnit.test('translates ranges in node', function(assert) {
},
resolvedUri: 'http://www.example.com/i.fmp4',
uri: 'http://www.example.com/i.fmp4',
+ presentationTime: 0,
number: 0
}]);
});
@@ -203,6 +215,7 @@ QUnit.test('generates playlist from sidx references', function(assert) {
},
duration: 2,
timeline: 0,
+ presentationTime: 0,
number: 0
}]);
});
diff --git a/test/segment/segmentTemplate.test.js b/test/segment/segmentTemplate.test.js
index f701eba3..31d731d3 100644
--- a/test/segment/segmentTemplate.test.js
+++ b/test/segment/segmentTemplate.test.js
@@ -248,9 +248,9 @@ QUnit.test('parseByDuration uses endNumber and has correct duration', function(a
sourceDuration: 11,
duration: '4',
periodIndex: 1,
- endNumber: '2',
type: 'static',
- periodStart: 0
+ periodStart: 0,
+ endNumber: '2'
};
assert.deepEqual(
diff --git a/test/toM3u8.test.js b/test/toM3u8.test.js
index ca1b52fd..967a15e6 100644
--- a/test/toM3u8.test.js
+++ b/test/toM3u8.test.js
@@ -12,6 +12,7 @@ QUnit.test('playlists', function(assert) {
duration: 0,
bandwidth: 20000,
periodIndex: 1,
+ periodStart: 0,
mimeType: 'audio/mp4',
type: 'static'
},
@@ -24,6 +25,7 @@ QUnit.test('playlists', function(assert) {
duration: 0,
bandwidth: 10000,
periodIndex: 1,
+ periodStart: 0,
mimeType: 'audio/mp4',
type: 'static'
},
@@ -38,6 +40,7 @@ QUnit.test('playlists', function(assert) {
duration: 0,
bandwidth: 10000,
periodIndex: 1,
+ periodStart: 0,
mimeType: 'video/mp4',
type: 'static'
},
@@ -48,6 +51,7 @@ QUnit.test('playlists', function(assert) {
id: '1',
bandwidth: 20000,
periodIndex: 1,
+ periodStart: 0,
mimeType: 'text/vtt',
type: 'static',
baseUrl: 'https://www.example.com/vtt'
@@ -58,6 +62,7 @@ QUnit.test('playlists', function(assert) {
id: '1',
bandwidth: 10000,
periodIndex: 1,
+ periodStart: 0,
mimeType: 'text/vtt',
type: 'static',
baseUrl: 'https://www.example.com/vtt'
@@ -83,7 +88,8 @@ QUnit.test('playlists', function(assert) {
NAME: '1',
['PROGRAM-ID']: 1
},
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
endList: true,
resolvedUri: '',
segments: [],
@@ -97,7 +103,8 @@ QUnit.test('playlists', function(assert) {
NAME: '2',
['PROGRAM-ID']: 1
},
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
endList: true,
resolvedUri: '',
segments: [],
@@ -124,6 +131,7 @@ QUnit.test('playlists', function(assert) {
['PROGRAM-ID']: 1
},
mediaSequence: 0,
+ discontinuitySequence: 0,
targetDuration: 100,
endList: true,
resolvedUri: 'https://www.example.com/vtt',
@@ -132,6 +140,7 @@ QUnit.test('playlists', function(assert) {
resolvedUri: 'https://www.example.com/vtt',
timeline: 1,
uri: 'https://www.example.com/vtt',
+ presentationTime: 0,
number: 0
}],
timeline: 1,
@@ -143,6 +152,7 @@ QUnit.test('playlists', function(assert) {
['PROGRAM-ID']: 1
},
mediaSequence: 0,
+ discontinuitySequence: 0,
targetDuration: 100,
endList: true,
resolvedUri: 'https://www.example.com/vtt',
@@ -151,6 +161,7 @@ QUnit.test('playlists', function(assert) {
resolvedUri: 'https://www.example.com/vtt',
timeline: 1,
uri: 'https://www.example.com/vtt',
+ presentationTime: 0,
number: 0
}],
timeline: 1,
@@ -176,7 +187,8 @@ QUnit.test('playlists', function(assert) {
}
},
endList: true,
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
targetDuration: 0,
resolvedUri: '',
segments: [],
@@ -187,7 +199,7 @@ QUnit.test('playlists', function(assert) {
uri: ''
};
- assert.deepEqual(toM3u8(input), expected);
+ assert.deepEqual(toM3u8({ dashPlaylists: input }), expected);
});
QUnit.test('playlists with segments', function(assert) {
@@ -375,7 +387,8 @@ QUnit.test('playlists with segments', function(assert) {
['PROGRAM-ID']: 1
},
targetDuration: 2,
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
endList: true,
resolvedUri: '',
segments: [{
@@ -387,7 +400,7 @@ QUnit.test('playlists with segments', function(assert) {
uri: '',
resolvedUri: ''
},
- number: 1
+ number: 0
}, {
uri: '',
timeline: 1,
@@ -397,7 +410,7 @@ QUnit.test('playlists with segments', function(assert) {
uri: '',
resolvedUri: ''
},
- number: 2
+ number: 1
}],
timeline: 1,
uri: ''
@@ -409,7 +422,8 @@ QUnit.test('playlists with segments', function(assert) {
['PROGRAM-ID']: 1
},
targetDuration: 2,
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
endList: true,
resolvedUri: '',
segments: [{
@@ -421,7 +435,7 @@ QUnit.test('playlists with segments', function(assert) {
uri: '',
resolvedUri: ''
},
- number: 1
+ number: 0
}, {
uri: '',
timeline: 1,
@@ -431,7 +445,7 @@ QUnit.test('playlists with segments', function(assert) {
uri: '',
resolvedUri: ''
},
- number: 2
+ number: 1
}],
timeline: 1,
uri: ''
@@ -455,7 +469,8 @@ QUnit.test('playlists with segments', function(assert) {
},
endList: true,
targetDuration: 2,
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
resolvedUri: 'https://www.example.com/vtt',
segments: [{
uri: '',
@@ -466,7 +481,7 @@ QUnit.test('playlists with segments', function(assert) {
uri: '',
resolvedUri: ''
},
- number: 1
+ number: 0
}, {
uri: '',
timeline: 1,
@@ -476,7 +491,7 @@ QUnit.test('playlists with segments', function(assert) {
uri: '',
resolvedUri: ''
},
- number: 2
+ number: 1
}],
timeline: 1,
uri: ''
@@ -488,7 +503,8 @@ QUnit.test('playlists with segments', function(assert) {
},
endList: true,
targetDuration: 2,
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
resolvedUri: 'https://www.example.com/vtt',
segments: [{
uri: '',
@@ -499,7 +515,7 @@ QUnit.test('playlists with segments', function(assert) {
uri: '',
resolvedUri: ''
},
- number: 1
+ number: 0
}, {
uri: '',
timeline: 1,
@@ -509,7 +525,7 @@ QUnit.test('playlists with segments', function(assert) {
uri: '',
resolvedUri: ''
},
- number: 2
+ number: 1
}],
timeline: 1,
uri: ''
@@ -535,7 +551,8 @@ QUnit.test('playlists with segments', function(assert) {
},
endList: true,
resolvedUri: '',
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
targetDuration: 2,
segments: [{
uri: '',
@@ -546,7 +563,7 @@ QUnit.test('playlists with segments', function(assert) {
uri: '',
resolvedUri: ''
},
- number: 1
+ number: 0
}, {
uri: '',
timeline: 1,
@@ -556,7 +573,7 @@ QUnit.test('playlists with segments', function(assert) {
uri: '',
resolvedUri: ''
},
- number: 2
+ number: 1
}],
timeline: 1,
uri: ''
@@ -565,7 +582,7 @@ QUnit.test('playlists with segments', function(assert) {
uri: ''
};
- assert.deepEqual(toM3u8(input), expected);
+ assert.deepEqual(toM3u8({ dashPlaylists: input }), expected);
});
QUnit.test('playlists with sidx and sidxMapping', function(assert) {
@@ -647,14 +664,23 @@ QUnit.test('playlists with sidx and sidxMapping', function(assert) {
resolvedUri: 'http://example.com/sidx.mp4',
duration: 2,
number: 0,
+ presentationTime: 0,
timeline: 1
}],
endList: true,
- mediaSequence: 1,
+ mediaSequence: 0,
+ discontinuitySequence: 0,
resolvedUri: ''
}];
- assert.deepEqual(toM3u8(input, null, mapping).playlists, expected);
+ assert.deepEqual(
+ toM3u8({
+ dashPlaylists: input,
+ locations: null,
+ sidxMapping: mapping
+ }).playlists,
+ expected
+ );
});
QUnit.test('playlists without minimumUpdatePeriod dont assign default value', function(assert) {
@@ -684,7 +710,7 @@ QUnit.test('playlists without minimumUpdatePeriod dont assign default value', fu
uri: 'http://example.com/fmp4.mp4'
}];
- assert.equal(toM3u8(input).minimumUpdatePeriod, undefined);
+ assert.equal(toM3u8({ dashPlaylists: input }).minimumUpdatePeriod, undefined);
});
QUnit.test('playlists with minimumUpdatePeriod = 0', function(assert) {
@@ -715,7 +741,7 @@ QUnit.test('playlists with minimumUpdatePeriod = 0', function(assert) {
uri: 'http://example.com/fmp4.mp4'
}];
- assert.equal(toM3u8(input).minimumUpdatePeriod, 0);
+ assert.equal(toM3u8({ dashPlaylists: input }).minimumUpdatePeriod, 0);
});
QUnit.test('playlists with integer value for minimumUpdatePeriod', function(assert) {
@@ -746,11 +772,15 @@ QUnit.test('playlists with integer value for minimumUpdatePeriod', function(asse
uri: 'http://example.com/fmp4.mp4'
}];
- assert.equal(toM3u8(input).minimumUpdatePeriod, 2000, 'converts update period to ms');
+ assert.equal(
+ toM3u8({ dashPlaylists: input }).minimumUpdatePeriod,
+ 2000,
+ 'converts update period to ms'
+ );
});
QUnit.test('no playlists', function(assert) {
- assert.deepEqual(toM3u8([]), {});
+ assert.deepEqual(toM3u8({ dashPlaylists: [] }), {});
});
QUnit.test('dynamic playlists with suggestedPresentationDelay', function(assert) {
@@ -815,7 +845,7 @@ QUnit.test('dynamic playlists with suggestedPresentationDelay', function(assert)
}
}];
- const output = toM3u8(input);
+ const output = toM3u8({ dashPlaylists: input });
assert.ok('suggestedPresentationDelay' in output);
assert.deepEqual(output.suggestedPresentationDelay, 18);
@@ -863,7 +893,7 @@ QUnit.test('playlists with label', function(assert) {
},
segments: []
}];
- const output = toM3u8(input);
+ const output = toM3u8({ dashPlaylists: input });
assert.ok(label in output.mediaGroups.AUDIO.audio, 'label exists');
});
@@ -921,9 +951,9 @@ QUnit.test('608 captions', function(assert) {
},
segments: []
}];
- const output = toM3u8(input);
+ const manifest = toM3u8({ dashPlaylists: input });
- const cc = output.mediaGroups['CLOSED-CAPTIONS'].cc;
+ const cc = manifest.mediaGroups['CLOSED-CAPTIONS'].cc;
Object.keys(cc).forEach((key) => {
assert.notOk(cc[key].autoselect, 'no autoselect');
diff --git a/test/toPlaylists.test.js b/test/toPlaylists.test.js
index 535f1905..d583abb4 100644
--- a/test/toPlaylists.test.js
+++ b/test/toPlaylists.test.js
@@ -54,6 +54,7 @@ QUnit.test('segment base', function(assert) {
attributes: {
baseUrl: 'http://example.com/',
periodIndex: 0,
+ periodStart: 0,
sourceDuration: 2,
type: 'static'
},
@@ -66,6 +67,7 @@ QUnit.test('segment base', function(assert) {
attributes: {
baseUrl: 'http://example.com/',
periodIndex: 0,
+ periodStart: 0,
sourceDuration: 2,
duration: 2,
type: 'static'
@@ -79,6 +81,7 @@ QUnit.test('segment base', function(assert) {
uri: 'http://example.com/',
timeline: 0,
duration: 2,
+ presentationTime: 0,
number: 0
}]
}];
@@ -91,6 +94,7 @@ QUnit.test('segment base with sidx', function(assert) {
attributes: {
baseUrl: 'http://example.com/',
periodIndex: 0,
+ periodStart: 0,
sourceDuration: 2,
indexRange: '10-19',
type: 'static'
@@ -104,6 +108,7 @@ QUnit.test('segment base with sidx', function(assert) {
attributes: {
baseUrl: 'http://example.com/',
periodIndex: 0,
+ periodStart: 0,
sourceDuration: 2,
duration: 2,
indexRange: '10-19',
@@ -123,6 +128,7 @@ QUnit.test('segment base with sidx', function(assert) {
},
timeline: 0,
duration: 2,
+ presentationTime: 0,
number: 0
}
}];
diff --git a/test/utils.test.js b/test/utils.test.js
index 222d8546..de9c366a 100644
--- a/test/utils.test.js
+++ b/test/utils.test.js
@@ -1,6 +1,13 @@
import { merge, values } from '../src/utils/object';
import { parseDuration } from '../src/utils/time';
-import { flatten, range, from, findIndexes } from '../src/utils/list';
+import {
+ flatten,
+ range,
+ from,
+ findIndex,
+ findIndexes,
+ includes
+} from '../src/utils/list';
import { findChildren, getContent } from '../src/utils/xml';
import {DOMParser} from 'xmldom';
import {JSDOM} from 'jsdom';
@@ -175,6 +182,24 @@ QUnit.test('array-like', function(assert) {
assert.deepEqual(result.length, 2);
});
+QUnit.module('findIndex');
+
+QUnit.test('returns first index that passes matching function', function(assert) {
+ assert.equal(
+ findIndex([1, { foo: 'bar' }, 1.5, 0, [], 3], (e) => e > 1),
+ 2,
+ 'returned correct index'
+ );
+});
+
+QUnit.test('returns -1 when nothing passes matching function', function(assert) {
+ assert.equal(
+ findIndex([1, { foo: 'bar' }, 1.5, 0, [], 3], (e) => e > 3),
+ -1,
+ 'returned -1 for no match'
+ );
+});
+
QUnit.module('findIndexes');
QUnit.test('index not found', function(assert) {
@@ -192,6 +217,23 @@ QUnit.test('indexes found', function(assert) {
], 'b'), [1, 2]);
});
+QUnit.module('includes');
+
+QUnit.test('returns true if element is found', function(assert) {
+ const element = { foo: 'bar' };
+
+ assert.ok(includes([1, {}, element], element), 'true if element is included');
+});
+
+QUnit.test('returns false if element is not found', function(assert) {
+ const element = { foo: 'bar' };
+
+ assert.notOk(
+ includes([1, {}, { foo: 'bar' }], element),
+ 'false if element is not included'
+ );
+});
+
QUnit.module('xml', {
beforeEach() {
const parser = new DOMParser();