Files
shaka-player/test/util/periods_unit.js
T
Joey Parrish 810f09ff1e fix(DASH): fix failure with identical text streams
The period-flattening algorithm is run even on single-period content
if the manifest is dynamic, because we don't know if another period
will be added later.

As a failsafe, we should create one output per input for single-period
content, and we can ensure that no streams are unused by marking an
input stream as the "best match" for an output if they share the same
ID.  That way, even if all other characteristics for two inputs are
the same, the input-to-output mapping for single-period content will
always be sane.

That ID-based logic was missing for text streams.  This change
corrects that.

Closes #2646

Change-Id: I28c6c63d92bcf710ae0072783911f9e66ed78228
2020-06-17 16:12:00 +00:00

834 lines
25 KiB
JavaScript

/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
describe('PeriodCombiner', () => {
// These test cases don't really read well as "it" statements. Phrasing them
// that way would make the names very long, so here we break with that
// convention.
/** @type {shaka.util.PeriodCombiner} */
let combiner;
beforeEach(() => {
combiner = new shaka.util.PeriodCombiner();
});
afterEach(() => {
combiner.release();
});
it('Ad insertion - join during main content', async () => {
/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: 'main',
videoStreams: [
makeVideoStream(1080),
makeVideoStream(720),
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('en', /* channels= */ 6),
makeAudioStream('en', /* channels= */ 2),
],
textStreams: [],
},
{
id: 'ad',
videoStreams: [
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('en', /* channels= */ 2),
],
textStreams: [],
},
];
// Start with the first period only.
await combiner.combinePeriods(periods.slice(0, 1), /* isDynamic= */ true);
expect(combiner.getVariants()).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(1080, 'en', /* channels= */ 6),
makeAVVariant(1080, 'en', /* channels= */ 2),
makeAVVariant(720, 'en', /* channels= */ 6),
makeAVVariant(720, 'en', /* channels= */ 2),
makeAVVariant(480, 'en', /* channels= */ 6),
makeAVVariant(480, 'en', /* channels= */ 2),
]));
// Add the second period.
await combiner.combinePeriods(periods, /* isDynamic= */ true);
const variants = combiner.getVariants();
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(1080, 'en', /* channels= */ 6),
makeAVVariant(1080, 'en', /* channels= */ 2),
makeAVVariant(720, 'en', /* channels= */ 6),
makeAVVariant(720, 'en', /* channels= */ 2),
makeAVVariant(480, 'en', /* channels= */ 6),
makeAVVariant(480, 'en', /* channels= */ 2),
]));
const h1080Surround = variants.find(
(v) => v.video.height == 1080 && v.audio.channelsCount == 6);
const h720Surround = variants.find(
(v) => v.video.height == 720 && v.audio.channelsCount == 6);
const h480Surround = variants.find(
(v) => v.video.height == 480 && v.audio.channelsCount == 6);
const h1080Stereo = variants.find(
(v) => v.video.height == 1080 && v.audio.channelsCount == 2);
const h720Stereo = variants.find(
(v) => v.video.height == 720 && v.audio.channelsCount == 2);
const h480Stereo = variants.find(
(v) => v.video.height == 480 && v.audio.channelsCount == 2);
// We can use the originalId field to see what each track is composed of.
expect(h1080Surround.video.originalId).toBe('1080,480');
expect(h1080Surround.audio.originalId).toBe('en-6c,en');
expect(h720Surround.video.originalId).toBe('720,480');
expect(h720Surround.audio.originalId).toBe('en-6c,en');
expect(h480Surround.video.originalId).toBe('480,480');
expect(h480Surround.audio.originalId).toBe('en-6c,en');
expect(h1080Stereo.video.originalId).toBe('1080,480');
expect(h1080Stereo.audio.originalId).toBe('en,en');
expect(h720Stereo.video.originalId).toBe('720,480');
expect(h720Stereo.audio.originalId).toBe('en,en');
expect(h480Stereo.video.originalId).toBe('480,480');
expect(h480Stereo.audio.originalId).toBe('en,en');
});
it('Ad insertion - join during ad', async () => {
/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: 'ad',
videoStreams: [
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('en', /* channels= */ 2),
],
textStreams: [],
},
{
id: 'main',
videoStreams: [
makeVideoStream(1080),
makeVideoStream(480),
makeVideoStream(720),
],
audioStreams: [
makeAudioStream('en', /* channels= */ 6),
makeAudioStream('en', /* channels= */ 2),
],
textStreams: [],
},
];
// Start with the first period only.
await combiner.combinePeriods(periods.slice(0, 1), /* isDynamic= */ true);
expect(combiner.getVariants()).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(480, 'en', /* channels= */ 2),
]));
// Add the second period.
await combiner.combinePeriods(periods, /* isDynamic= */ true);
const variants = combiner.getVariants();
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(1080, 'en', /* channels= */ 6),
makeAVVariant(1080, 'en', /* channels= */ 2),
makeAVVariant(720, 'en', /* channels= */ 6),
makeAVVariant(720, 'en', /* channels= */ 2),
makeAVVariant(480, 'en', /* channels= */ 6),
makeAVVariant(480, 'en', /* channels= */ 2),
]));
const h1080Surround = variants.find(
(v) => v.video.height == 1080 && v.audio.channelsCount == 6);
const h720Surround = variants.find(
(v) => v.video.height == 720 && v.audio.channelsCount == 6);
const h480Surround = variants.find(
(v) => v.video.height == 480 && v.audio.channelsCount == 6);
const h1080Stereo = variants.find(
(v) => v.video.height == 1080 && v.audio.channelsCount == 2);
const h720Stereo = variants.find(
(v) => v.video.height == 720 && v.audio.channelsCount == 2);
const h480Stereo = variants.find(
(v) => v.video.height == 480 && v.audio.channelsCount == 2);
// We can use the originalId field to see what each track is composed of.
expect(h1080Surround.video.originalId).toBe('480,1080');
expect(h1080Surround.audio.originalId).toBe('en,en-6c');
expect(h720Surround.video.originalId).toBe('480,720');
expect(h720Surround.audio.originalId).toBe('en,en-6c');
expect(h480Surround.video.originalId).toBe('480,480');
expect(h480Surround.audio.originalId).toBe('en,en-6c');
expect(h1080Stereo.video.originalId).toBe('480,1080');
expect(h1080Stereo.audio.originalId).toBe('en,en');
expect(h720Stereo.video.originalId).toBe('480,720');
expect(h720Stereo.audio.originalId).toBe('en,en');
expect(h480Stereo.video.originalId).toBe('480,480');
expect(h480Stereo.audio.originalId).toBe('en,en');
});
it('Ad insertion - smaller ad, res not found in main content', async () => {
/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
makeVideoStream(720),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
},
];
// Start with the first period only.
await combiner.combinePeriods(periods.slice(0, 1), /* isDynamic= */ true);
expect(combiner.getVariants()).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(1080, 'en'),
makeAVVariant(720, 'en'),
]));
// Add the second period.
await combiner.combinePeriods(periods, /* isDynamic= */ true);
const variants = combiner.getVariants();
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(1080, 'en'),
makeAVVariant(720, 'en'),
]));
// We can use the originalId field to see what each track is composed of.
const h1080 = variants.find((v) => v.video.height == 1080);
const h720 = variants.find((v) => v.video.height == 720);
expect(h1080.video.originalId).toBe('1080,480');
expect(h720.video.originalId).toBe('720,480');
});
it('Ad insertion - larger ad, res not found in main content', async () => {
/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(720),
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
},
];
// Start with the first period only.
await combiner.combinePeriods(periods.slice(0, 1), /* isDynamic= */ true);
expect(combiner.getVariants()).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(720, 'en'),
makeAVVariant(480, 'en'),
]));
// Add the second period.
await combiner.combinePeriods(periods, /* isDynamic= */ true);
const variants = combiner.getVariants();
// We don't know what order they will be in, but both tracks got "upgraded"
// to 1080p by the ad. We would consider this scenario unusual. The
// strange results are because we can't tell the difference between joining
// a presentation during an ad or during the main content, so we don't know
// which Period's resolutions would be more "authoritative" for the track
// list.
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(1080, 'en'),
makeAVVariant(1080, 'en'),
]));
// We can use the originalId field to see what each track is composed of.
const originalIds = variants.map((v) => v.video.originalId);
expect(originalIds).toEqual(jasmine.arrayWithExactContents([
'720,1080',
'480,1080',
]));
});
it('Language changes during and after an ad', async () => {
/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: 'show1',
videoStreams: [
makeVideoStream(1080),
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('es', /* channels= */ 2, /* primary= */ true),
makeAudioStream('fr', /* channels= */ 2, /* primary= */ false),
],
textStreams: [],
},
{
id: 'ad',
videoStreams: [
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
},
{
id: 'show2',
videoStreams: [
makeVideoStream(1080),
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('es', /* channels= */ 2, /* primary= */ false),
makeAudioStream('fr', /* channels= */ 2, /* primary= */ true),
],
textStreams: [],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ false);
const variants = combiner.getVariants();
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(1080, 'es'),
makeAVVariant(480, 'es'),
makeAVVariant(1080, 'en'),
makeAVVariant(480, 'en'),
makeAVVariant(1080, 'fr'),
makeAVVariant(480, 'fr'),
]));
// We can use the originalId field to see what each track is composed of.
// The Spanish track is primary in the first period and has English 480p in
// the middle period.
const spanish = variants.find(
(v) => v.video.height == 1080 && v.language == 'es');
expect(spanish.audio.originalId).toBe('es*,en,es');
expect(spanish.video.originalId).toBe('1080,480,1080');
// The French track is primary in the last period and has English 480p in
// the middle period.
const french = variants.find(
(v) => v.video.height == 1080 && v.language == 'fr');
expect(french.audio.originalId).toBe('fr,en,fr*');
expect(french.video.originalId).toBe('1080,480,1080');
// Because there's no English in the first or last periods, the English
// track follows the "primary" language in those periods.
const english = variants.find(
(v) => v.video.height == 1080 && v.language == 'en');
expect(english.audio.originalId).toBe('es*,en,fr*');
expect(english.video.originalId).toBe('1080,480,1080');
});
it('VOD playlist of completely unrelated periods', async () => {
/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: 'show1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('es'),
],
textStreams: [],
},
{
id: 'show2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ false);
const variants = combiner.getVariants();
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(1080, 'es'),
makeAVVariant(1080, 'en'),
]));
// We can use the originalId field to see what each track is composed of.
// Both tracks are composed of the same things.
const spanish = variants.find(
(v) => v.video.height == 1080 && v.language == 'es');
const english = variants.find(
(v) => v.video.height == 1080 && v.language == 'en');
expect(spanish.audio.originalId).toBe('es,en');
expect(english.audio.originalId).toBe('es,en');
});
it('Multiple representations of the same resolution', async () => {
/** @type {shaka.extern.Stream} */
const video1 = makeVideoStream(480);
video1.bandwidth = 1;
/** @type {shaka.extern.Stream} */
const video2 = makeVideoStream(480);
video2.bandwidth = 2;
/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: '1',
videoStreams: [
video1,
video2,
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ true);
const variants = combiner.getVariants();
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(480, 'en', /* channels= */ 2),
makeAVVariant(480, 'en', /* channels= */ 2),
]));
const lowBandwidth = variants.find(
(v) => v.video.height == 480 && v.bandwidth == 1);
const highBandwidth = variants.find(
(v) => v.video.height == 480 && v.bandwidth == 2);
expect(lowBandwidth.video.originalId).toBe('480');
expect(highBandwidth.video.originalId).toBe('480');
});
it('Text track gaps', async () => {
/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [
makeTextStream('en'),
],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [
/* No text streams */
],
},
{
id: '3',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [
makeTextStream('en'),
makeTextStream('es'),
],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ false);
const textStreams = combiner.getTextStreams();
expect(textStreams).toEqual(jasmine.arrayWithExactContents([
jasmine.objectContaining({
language: 'es',
}),
jasmine.objectContaining({
language: 'en',
}),
]));
// We can use the originalId field to see what each track is composed of.
// Both tracks are composed of the same things.
const spanish = textStreams.find((s) => s.language == 'es');
const english = textStreams.find((s) => s.language == 'en');
expect(spanish.originalId).toBe(',,es');
expect(english.originalId).toBe('en,,en');
});
it('Disjoint audio channels', async () => {
/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en', /* channels= */ 6),
],
textStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en', /* channels= */ 2),
],
textStreams: [],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ false);
const variants = combiner.getVariants();
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(1080, 'en', /* channels= */ 6),
]));
// We can use the originalId field to see what each track is composed of.
// There is only one track. The whole thing gets "upgraded" to 6-channel
// surround.
const audio = variants[0].audio;
expect(audio.originalId).toBe('en-6c,en');
});
it('Disjoint audio sample rates, ascending order', async () => {
const makeAudioStreamWithSampleRate = (rate) => {
const stream = makeAudioStream('en');
stream.audioSamplingRate = rate;
stream.originalId = rate.toString();
return stream;
};
/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithSampleRate(48000),
],
textStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithSampleRate(44100),
],
textStreams: [],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ false);
const variants = combiner.getVariants();
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(1080, 'en'),
]));
// We can use the originalId field to see what each track is composed of.
// There is only one track.
const audio = variants[0].audio;
expect(audio.audioSamplingRate).toBe(48000);
expect(audio.originalId).toBe('48000,44100');
});
it('Disjoint audio sample rates, descending order', async () => {
const makeAudioStreamWithSampleRate = (rate) => {
const stream = makeAudioStream('en');
stream.audioSamplingRate = rate;
stream.originalId = rate.toString();
return stream;
};
/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithSampleRate(44100),
],
textStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithSampleRate(48000),
],
textStreams: [],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ false);
const variants = combiner.getVariants();
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(1080, 'en'),
]));
// We can use the originalId field to see what each track is composed of.
// There is only one track.
const audio = variants[0].audio;
expect(audio.audioSamplingRate).toBe(48000);
expect(audio.originalId).toBe('44100,48000');
});
it('Matches streams with most roles in common', async () => {
const makeAudioStreamWithRoles = (roles) => {
const stream = makeAudioStream('en');
stream.roles = roles;
return stream;
};
const stream1 = makeAudioStreamWithRoles(['role1', 'role2']);
stream1.originalId = 'stream1';
const stream2 = makeAudioStreamWithRoles(['role1']);
stream2.originalId = 'stream2';
const stream3 = makeAudioStreamWithRoles(['role1', 'role2']);
stream3.originalId = 'stream3';
const stream4 = makeAudioStreamWithRoles(['role1']);
stream4.originalId = 'stream4';
/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
stream1,
stream2,
],
textStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
stream3,
stream4,
],
textStreams: [],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ false);
const variants = combiner.getVariants();
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(1080, 'en'),
makeAVVariant(1080, 'en'),
]));
// We can use the originalId field to see what each track is composed of.
const audio1 = variants[0].audio;
expect(audio1.roles).toEqual(['role1', 'role2']);
expect(audio1.originalId).toBe('stream1,stream3');
const audio2 = variants[1].audio;
expect(audio2.roles).toEqual(['role1']);
expect(audio2.originalId).toBe('stream2,stream4');
});
it('Identical streams except for ID', async () => {
// When two video or audio or text streams have the same characteristics,
// this should at least succeed for single-period live streams. We can
// create an output for each input based purely on their IDs.
/** @type {shaka.extern.Stream} */
const video1 = makeVideoStream(480);
/** @type {shaka.extern.Stream} */
const video2 = makeVideoStream(480);
/** @type {shaka.extern.Stream} */
const audio1 = makeAudioStream('en');
/** @type {shaka.extern.Stream} */
const audio2 = makeAudioStream('en');
/** @type {shaka.extern.Stream} */
const text1 = makeTextStream('es');
/** @type {shaka.extern.Stream} */
const text2 = makeTextStream('es');
/** @type {!Array.<shaka.util.PeriodCombiner.Period>} */
const periods = [
{
id: '1',
videoStreams: [
video1,
video2,
],
audioStreams: [
audio1,
audio2,
],
textStreams: [
text1,
text2,
],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ true);
const variants = combiner.getVariants();
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(480, 'en', /* channels= */ 2),
makeAVVariant(480, 'en', /* channels= */ 2),
makeAVVariant(480, 'en', /* channels= */ 2),
makeAVVariant(480, 'en', /* channels= */ 2),
]));
const textStreams = combiner.getTextStreams();
expect(textStreams).toEqual(jasmine.arrayWithExactContents([
jasmine.objectContaining({
language: 'es',
}),
jasmine.objectContaining({
language: 'es',
}),
]));
});
/** @type {number} */
let nextId = 0;
/**
* @param {number} height
* @return {shaka.extern.Stream}
* @suppress {accessControls}
*/
function makeVideoStream(height) {
const width = height * 4 / 3;
const streamGenerator = new shaka.test.ManifestGenerator.Stream(
/* manifest= */ null,
/* isPartial= */ false,
/* id= */ nextId++,
/* type= */ shaka.util.ManifestParserUtils.ContentType.VIDEO,
/* lang= */ 'und');
streamGenerator.size(width, height);
streamGenerator.originalId = height.toString();
return streamGenerator.build_();
}
/**
* @param {string} language
* @param {number=} channels
* @param {boolean=} primary
* @return {shaka.extern.Stream}
* @suppress {accessControls}
*/
function makeAudioStream(language, channels = 2, primary = false) {
const streamGenerator = new shaka.test.ManifestGenerator.Stream(
/* manifest= */ null,
/* isPartial= */ false,
/* id= */ nextId++,
/* type= */ shaka.util.ManifestParserUtils.ContentType.AUDIO,
language);
streamGenerator.primary = primary;
streamGenerator.channelsCount = channels;
streamGenerator.originalId = primary ? language + '*' : language;
if (channels != 2) {
streamGenerator.originalId += `-${channels}c`;
}
return streamGenerator.build_();
}
/**
* @param {string} language
* @param {boolean=} primary
* @return {shaka.extern.Stream}
* @suppress {accessControls}
*/
function makeTextStream(language, primary = false) {
const streamGenerator = new shaka.test.ManifestGenerator.Stream(
/* manifest= */ null,
/* isPartial= */ false,
/* id= */ nextId++,
/* type= */ shaka.util.ManifestParserUtils.ContentType.TEXT,
language);
streamGenerator.primary = primary;
streamGenerator.originalId = primary ? language + '*' : language;
return streamGenerator.build_();
}
/**
* @param {number} height
* @param {string} language
* @param {number=} channels
* @return {shaka.extern.Variant}
*/
function makeAVVariant(height, language, channels = 2) {
const variant = jasmine.objectContaining({
language,
audio: jasmine.objectContaining({
language,
channelsCount: channels,
}),
video: jasmine.objectContaining({
height,
}),
});
return /** @type {shaka.extern.Variant} */(/** @type {?} */(variant));
}
});