shaka-player
Version:
DASH/EME video player library
1,624 lines (1,444 loc) • 47.5 kB
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;
const makeAudioStreamWithRoles = (id, roles, primary = true) => {
const stream = makeAudioStream('en');
stream.originalId = id;
stream.roles = roles;
stream.primary = primary;
return stream;
};
beforeEach(() => {
combiner = new shaka.util.PeriodCombiner();
});
afterEach(() => {
combiner.release();
});
it('Ad insertion - join during main content', async () => {
/** @type {!Array.<shaka.extern.Period>} */
const periods = [
{
id: 'main',
videoStreams: [
makeVideoStream(1080),
makeVideoStream(720),
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('en', /* channels= */ 6),
makeAudioStream('en', /* channels= */ 2),
],
textStreams: [],
imageStreams: [],
},
{
id: 'ad',
videoStreams: [
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('en', /* channels= */ 2),
],
textStreams: [],
imageStreams: [],
},
];
// 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.extern.Period>} */
const periods = [
{
id: 'ad',
videoStreams: [
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('en', /* channels= */ 2),
],
textStreams: [],
imageStreams: [],
},
{
id: 'main',
videoStreams: [
makeVideoStream(1080),
makeVideoStream(480),
makeVideoStream(720),
],
audioStreams: [
makeAudioStream('en', /* channels= */ 6),
makeAudioStream('en', /* channels= */ 2),
],
textStreams: [],
imageStreams: [],
},
];
// 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.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
makeVideoStream(720),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
imageStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
imageStreams: [],
},
];
// 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.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(720),
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
imageStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
imageStreams: [],
},
];
// 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();
expect(variants.length).toBe(2);
// 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.extern.Period>} */
const periods = [
{
id: 'show1',
videoStreams: [
makeVideoStream(1080),
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('es', /* channels= */ 2, /* primary= */ true),
makeAudioStream('fr', /* channels= */ 2, /* primary= */ false),
],
textStreams: [],
imageStreams: [],
},
{
id: 'ad',
videoStreams: [
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
imageStreams: [],
},
{
id: 'show2',
videoStreams: [
makeVideoStream(1080),
makeVideoStream(480),
],
audioStreams: [
makeAudioStream('es', /* channels= */ 2, /* primary= */ false),
makeAudioStream('fr', /* channels= */ 2, /* primary= */ true),
],
textStreams: [],
imageStreams: [],
},
];
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.audio.originalLanguage).toBe('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.audio.originalLanguage).toBe('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.audio.originalLanguage).toBe('en');
expect(english.video.originalId).toBe('1080,480,1080');
});
it('VOD playlist of completely unrelated periods', async () => {
/** @type {!Array.<shaka.extern.Period>} */
const periods = [
{
id: 'show1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('es'),
],
textStreams: [],
imageStreams: [],
},
{
id: 'show2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
imageStreams: [],
},
];
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(spanish.audio.originalLanguage).toBe('es');
expect(english.audio.originalId).toBe('es,en');
expect(english.audio.originalLanguage).toBe('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.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [
video1,
video2,
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
imageStreams: [],
},
];
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('Filters out duplicate streams', async () => {
// v1 and v3 are duplicates
const v1 = makeVideoStream(1280);
v1.frameRate = 30000/1001;
v1.originalId = 'v1';
v1.bandwidth = 6200000;
const v2 = makeVideoStream(1920);
v2.frameRate = 30000/1001;
v2.originalId = 'v2';
v2.bandwidth = 8000000;
const v3 = makeVideoStream(1280);
v3.frameRate = 30000/1001;
v3.originalId = 'v3';
v3.bandwidth = 6200000;
// a1 and a2 are duplicates.
const a1 = makeAudioStream('en', /* channels= */ 2);
a1.originalId = 'a1';
a1.bandwidth = 65106;
a1.roles = ['role1', 'role2'];
a1.codecs = 'mp4a.40.2';
const a2 = makeAudioStream('en', /* channels= */ 2);
a2.originalId = 'a2';
a2.bandwidth = 65106;
a2.roles = ['role1', 'role2'];
a2.codecs = 'mp4a.40.2';
const a3 = makeAudioStream('en', /* channels= */ 2);
a3.originalId = 'a3';
a3.bandwidth = 97065;
a3.roles = ['role1', 'role2'];
a3.codecs = 'mp4a.40.2';
// a4 has a different label from a3, and should not
// be filtered out.
const a4 = makeAudioStream('en', /* channels= */ 2);
a4.originalId = 'a4';
a4.bandwidth = 97065;
a4.roles = ['role1', 'role2'];
a4.label = 'Surround';
a4.codecs = 'mp4a.40.2';
// a5 has a different codec from a3, and should not
// be filtered out.
const a5 = makeAudioStream('en', /* channels= */ 2);
a5.originalId = 'a5';
a5.bandwidth = 97065;
a5.roles = ['role1', 'role2'];
a5.codecs = 'ec-3';
// t1 and t3 are duplicates.
const t1 = makeTextStream('en');
t1.originalId = 't1';
t1.roles = ['role1'];
t1.bandwidth = 1158;
const t2 = makeTextStream('en');
t2.originalId = 't2';
t2.roles = ['role1', 'role2'];
t2.bandwidth = 1172;
const t3 = makeTextStream('en');
t3.originalId = 't3';
t3.roles = ['role1'];
t3.bandwidth = 1158;
// t4 has a different bandwidth from t3, and should not
// be filtered out.
const t4 = makeTextStream('en');
t4.originalId = 't4';
t4.roles = ['role1'];
t4.bandwidth = 1186;
// i1 and i3 are duplicates.
const i1 = makeImageStream(240);
i1.originalId = 'i1';
const i2 = makeImageStream(480);
i2.originalId = 'i2';
const i3 = makeImageStream(240);
i3.originalId = 'i3';
/** @type {!Array.<shaka.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [
v1,
v2,
v3,
],
audioStreams: [
a1,
a2,
a3,
a4,
a5,
],
textStreams: [
t1,
t2,
t3,
t4,
],
imageStreams: [
i1,
i2,
i3,
],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ true);
const variants = combiner.getVariants();
expect(variants.length).toBe(8);
// v3 should've been filtered out
const videoIds = variants.map((v) => v.video.originalId);
for (const id of videoIds) {
expect(id).not.toBe('v3');
}
// a2 should've been filtered out
const audioIds = variants.map((v) => v.audio.originalId);
for (const id of audioIds) {
expect(id).not.toBe('a2');
}
const textStreams = combiner.getTextStreams();
expect(textStreams.length).toBe(3);
// t3 should've been filtered out
const textIds = textStreams.map((t) => t.originalId);
for (const id of textIds) {
expect(id).not.toBe('t3');
}
const imageStreams = combiner.getImageStreams();
expect(imageStreams.length).toBe(2);
// i3 should've been filtered out
const imageIds = imageStreams.map((i) => i.originalId);
for (const id of imageIds) {
expect(id).not.toBe('i3');
}
});
// Regression test for #3383, where we failed on multi-period content with
// multiple image streams per period.
it('Can handle multiple image streams', async () => {
/** @type {!Array.<shaka.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1280),
],
audioStreams: [],
textStreams: [],
imageStreams: [
makeImageStream(240),
makeImageStream(480),
],
},
{
id: '2',
videoStreams: [
makeVideoStream(1280),
],
audioStreams: [],
textStreams: [],
imageStreams: [
makeImageStream(240),
makeImageStream(480),
],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ true);
const imageStreams = combiner.getImageStreams();
expect(imageStreams.length).toBe(2);
const imageIds = imageStreams.map((i) => i.originalId);
expect(imageIds).toEqual([
'240,240',
'480,480',
]);
});
it('handles text track gaps', async () => {
/** @type {!Array.<shaka.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [
makeTextStream('en'),
],
imageStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [
/* No text streams */
],
imageStreams: [],
},
{
id: '3',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [
makeTextStream('en'),
makeTextStream('spa'),
],
imageStreams: [],
},
];
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(spanish.originalLanguage).toBe('spa');
expect(english.originalId).toBe('en,,en');
expect(english.originalLanguage).toBe('en');
});
it('handles image track gaps', async () => {
/** @type {!Array.<shaka.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
imageStreams: [
makeImageStream(240),
],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
imageStreams: [
/* No image streams in this period */
],
},
{
id: '3',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en'),
],
textStreams: [],
imageStreams: [
makeImageStream(240),
makeImageStream(480),
],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ false);
const imageStreams = combiner.getImageStreams();
expect(imageStreams).toEqual(jasmine.arrayWithExactContents([
jasmine.objectContaining({
height: 240,
}),
jasmine.objectContaining({
height: 480,
}),
]));
// We can use the originalId field to see what each track is composed of.
const i240 = imageStreams.find((s) => s.height == 240);
const i480 = imageStreams.find((s) => s.height == 480);
expect(i240.originalId).toBe('240,,240');
expect(i480.originalId).toBe('240,,480');
});
it('Disjoint audio channels', async () => {
/** @type {!Array.<shaka.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en', /* channels= */ 6),
],
textStreams: [],
imageStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStream('en', /* channels= */ 2),
],
textStreams: [],
imageStreams: [],
},
];
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.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithSampleRate(48000),
],
textStreams: [],
imageStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithSampleRate(44100),
],
textStreams: [],
imageStreams: [],
},
];
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.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithSampleRate(44100),
],
textStreams: [],
imageStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithSampleRate(48000),
],
textStreams: [],
imageStreams: [],
},
];
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(44100);
expect(audio.originalId).toBe('44100,48000');
});
it('ignores newly added codecs', async () => {
const newCodec = makeVideoStream(720);
newCodec.codecs = 'foo.abcd';
/** @type {!Array.<shaka.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
newCodec,
],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ false);
const variants = combiner.getVariants();
expect(variants.length).toBe(1);
});
it('Matches streams with no roles', async () => {
const stream1 = makeAudioStream('en', /* channels= */ 2);
stream1.originalId = '1';
stream1.bandwidth = 129597;
stream1.codecs = 'mp4a.40.2';
const stream2 = makeAudioStream('en', /* channels= */ 2);
stream2.originalId = '2';
stream2.bandwidth = 129637;
stream2.codecs = 'mp4a.40.2';
stream2.roles = ['description'];
const stream3 = makeAudioStream('en', /* channels= */ 2);
stream3.originalId = '3';
stream3.bandwidth = 131037;
stream3.codecs = 'mp4a.40.2';
const stream4 = makeAudioStream('en', /* channels= */ 2);
stream4.originalId = '4';
stream4.bandwidth = 131034;
stream4.codecs = 'mp4a.40.2';
stream4.roles = ['description'];
/** @type {!Array.<shaka.extern.Period>} */
const periods = [
{
id: '0',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
stream1,
stream2,
],
textStreams: [],
imageStreams: [],
},
{
id: '1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
stream3,
stream4,
],
textStreams: [],
imageStreams: [],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ true);
const variants = combiner.getVariants();
expect(variants.length).toBe(2);
// We can use the originalId field to see what each track is composed of.
const audio1 = variants[0].audio;
expect(audio1.roles).toEqual([]);
expect(audio1.originalId).toBe('1,3');
const audio2 = variants[1].audio;
expect(audio2.roles).toEqual(['description']);
expect(audio2.originalId).toBe('2,4');
});
it('Matches streams with related codecs', async () => {
const stream1 = makeVideoStream(1080);
stream1.originalId = '1';
stream1.bandwidth = 120000;
stream1.codecs = 'hvc1.1.4.L126.B0';
const stream2 = makeVideoStream(1080);
stream2.originalId = '2';
stream2.bandwidth = 120000;
stream2.codecs = 'hev1.2.4.L123.B0';
const stream3 = makeVideoStream(1080);
stream3.originalId = '3';
stream3.bandwidth = 120000;
stream3.codecs = 'dvhe.05.01';
const stream4 = makeVideoStream(1080);
stream4.originalId = '4';
stream4.bandwidth = 120000;
stream4.codecs = 'dvh1.05.01';
const stream5 = makeVideoStream(1080);
stream5.originalId = '5';
stream5.bandwidth = 120000;
stream5.codecs = 'avc1.42001f';
const stream6 = makeVideoStream(1080);
stream6.originalId = '6';
stream6.bandwidth = 120000;
stream6.codecs = 'avc3.42001f';
const stream7 = makeVideoStream(1080);
stream7.originalId = '7';
stream7.bandwidth = 120000;
stream7.codecs = 'vp09.00.10.08';
const stream8 = makeVideoStream(1080);
stream8.originalId = '8';
stream8.bandwidth = 120000;
stream8.codecs = 'vp09.01.20.08.01';
/** @type {!Array.<shaka.extern.Period>} */
const periods = [
{
id: '0',
videoStreams: [
stream1, stream3, stream5, stream7,
],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
{
id: '1',
videoStreams: [
stream2, stream4, stream6, stream8,
],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ true);
const variants = combiner.getVariants();
expect(variants.length).toBe(4);
// We can use the originalId field to see what each track is composed of.
const video1 = variants[0].video;
expect(video1.originalId).toBe('1,2');
const video2 = variants[1].video;
expect(video2.originalId).toBe('3,4');
const video3 = variants[2].video;
expect(video3.originalId).toBe('5,6');
const video4 = variants[3].video;
expect(video4.originalId).toBe('7,8');
});
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.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
stream1,
stream2,
],
textStreams: [],
imageStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(1080),
],
audioStreams: [
stream3,
stream4,
],
textStreams: [],
imageStreams: [],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ false);
const variants = combiner.getVariants();
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(1080, 'en', 2, ['role1', 'role2']),
makeAVVariant(1080, 'en', 2, ['role1']),
]));
// We can use the originalId field to see what each track is composed of.
expect(variants[0].audio.originalId).toBe('stream1,stream3');
expect(variants[1].audio.originalId).toBe('stream2,stream4');
});
it('Matches streams with roles in common', async () => {
/** @type {!Array.<shaka.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [
makeVideoStream(720),
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithRoles('stream1', ['main']),
makeAudioStreamWithRoles('stream2', ['description'], false),
],
textStreams: [],
imageStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(720),
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithRoles('stream1', ['main']),
makeAudioStreamWithRoles('stream2', ['description'], false),
],
textStreams: [],
imageStreams: [],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ false);
const variants = combiner.getVariants();
console.log(variants);
expect(variants.length).toBe(4);
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(720, 'en', 2, ['main']),
makeAVVariant(1080, 'en', 2, ['main']),
makeAVVariant(720, 'en', 2, ['description']),
makeAVVariant(1080, 'en', 2, ['description']),
]));
// We can use the originalId field to see what each track is composed of.
expect(variants[0].audio.originalId).toBe('stream1,stream1');
expect(variants[1].audio.originalId).toBe('stream1,stream1');
expect(variants[2].audio.originalId).toBe('stream2,stream2');
expect(variants[3].audio.originalId).toBe('stream2,stream2');
});
it('Matches streams with mismatched roles', async () => {
/** @type {!Array.<shaka.extern.Period>} */
const periods = [
{
id: '0',
videoStreams: [
makeVideoStream(720),
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithRoles('stream1', ['main']),
],
textStreams: [],
imageStreams: [],
},
{
id: '1',
videoStreams: [
makeVideoStream(720),
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithRoles('stream1', ['main']),
makeAudioStreamWithRoles('stream2', ['description'], false),
],
textStreams: [],
imageStreams: [],
},
{
id: '2',
videoStreams: [
makeVideoStream(720),
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithRoles('stream1', ['main']),
],
textStreams: [],
imageStreams: [],
},
{
id: '3',
videoStreams: [
makeVideoStream(720),
makeVideoStream(1080),
],
audioStreams: [
makeAudioStreamWithRoles('stream1', ['main']),
makeAudioStreamWithRoles('stream2', ['description'], false),
],
textStreams: [],
imageStreams: [],
},
];
await combiner.combinePeriods(periods, /* isDynamic= */ false);
const variants = combiner.getVariants();
console.log(variants);
expect(variants.length).toBe(4);
expect(variants).toEqual(jasmine.arrayWithExactContents([
makeAVVariant(720, 'en', 2, ['main']),
makeAVVariant(1080, 'en', 2, ['main']),
makeAVVariant(720, 'en', 2, ['description', 'main']),
makeAVVariant(1080, 'en', 2, ['description', 'main']),
]));
// We can use the originalId field to see what each track is composed of.
expect(variants[0].audio.originalId)
.toBe('stream1,stream1,stream1,stream1');
expect(variants[1].audio.originalId)
.toBe('stream1,stream1,stream1,stream1');
expect(variants[2].audio.originalId)
.toBe('stream1,stream2,stream1,stream2');
expect(variants[3].audio.originalId)
.toBe('stream1,stream2,stream1,stream2');
});
it('The number of variants stays stable after many periods ' +
'when going between similar content and varying ads', async () => {
// This test is based on the content from
// https://github.com/shaka-project/shaka-player/issues/2716
// that used to cause our period flattening logic to keep
// creating new variants for every new period added.
// It's ok to create a few additional variants/streams,
// but we should stabilize eventually and keep the number
// of variants from growing indefinitely.
// 1st period streams
const v1 = makeVideoStream(720);
v1.frameRate = 30000/1001;
v1.bandwidth = 6200000;
const v2 = makeVideoStream(1080);
v2.frameRate = 30000/1001;
v2.bandwidth = 8000000;
const v3 = makeVideoStream(272);
v3.frameRate = 15000/1001;
v3.bandwidth = 400000;
const v4 = makeVideoStream(360);
v4.frameRate = 30000/1001;
v4.bandwidth = 800000;
const v5 = makeVideoStream(432);
v5.frameRate = 30000/1001;
v5.bandwidth = 1200000;
// 2nd period streams
const v6 = makeVideoStream(432);
v6.frameRate = 24000/1001;
v6.bandwidth = 933000;
const v7 = makeVideoStream(360);
v7.frameRate = 30000/1001;
v7.bandwidth = 363000;
const v8 = makeVideoStream(540);
v8.frameRate = 15000/1001;
v8.bandwidth = 1780000;
const v9 = makeVideoStream(720);
v9.frameRate = 30000/1001;
v9.bandwidth = 3940000;
const v10 = makeVideoStream(360);
v10.frameRate = 30000/1001;
v10.bandwidth = 599000;
// 3nd period streams
const v11 = makeVideoStream(432);
v11.frameRate = 24000/1001;
v11.bandwidth = 894000;
const v12 = makeVideoStream(360);
v12.frameRate = 30000/1001;
v12.bandwidth = 365000;
const v13 = makeVideoStream(540);
v13.frameRate = 15000/1001;
v13.bandwidth = 1611000;
const v14 = makeVideoStream(720);
v14.frameRate = 30000/1001;
v14.bandwidth = 3967000;
const v15 = makeVideoStream(360);
v15.frameRate = 30000/1001;
v15.bandwidth = 570000;
// 4th period streams
const v16 = makeVideoStream(432);
v16.frameRate = 24000/1001;
v16.bandwidth = 933000;
const v17 = makeVideoStream(360);
v17.frameRate = 24000/1001;
v17.bandwidth = 363000;
const v18 = makeVideoStream(540);
v18.frameRate = 24000/1001;
v18.bandwidth = 1780000;
const v19 = makeVideoStream(720);
v19.frameRate = 24000/1001;
v19.bandwidth = 3940000;
const v20 = makeVideoStream(360);
v20.frameRate = 24000/1001;
v20.bandwidth = 570005990000;
/** @type {!Array.<shaka.extern.Period>} */
const periods = [
{
id: '1',
videoStreams: [v1, v2, v3, v4, v5],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
{
id: '2',
videoStreams: [v6, v7, v8, v9, v10],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
{
id: '3',
videoStreams: [v11, v12, v13, v14, v15],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
{
id: '4',
videoStreams: [v16, v17, v18, v19, v20],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
{
id: '5',
// Same as 1st
videoStreams: [v1, v2, v3, v4, v5],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
{
id: '6',
// Same as 2nd
videoStreams: [v6, v7, v8, v9, v10],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
{
id: '7',
// Same as 3rd
videoStreams: [v11, v12, v13, v14, v15],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
{
id: '8',
// Same as 4th
videoStreams: [v16, v17, v18, v19, v20],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
// Adding the 1st period again since it was the one that used to
// cause trouble when repeated.
{
id: '9',
// Same as 1st and 5th
videoStreams: [v1, v2, v3, v4, v5],
audioStreams: [],
textStreams: [],
imageStreams: [],
},
];
await combiner.combinePeriods(periods.slice(0, 4), /* isDynamic= */ true);
const variantsAfter4Periods = combiner.getVariants();
await combiner.combinePeriods(periods.slice(0, 8), /* isDynamic= */ true);
const variantsAfter8Periods = combiner.getVariants();
expect(variantsAfter4Periods).toEqual(variantsAfter8Periods);
await combiner.combinePeriods(periods, /* isDynamic= */ true);
const variantsAfterAllPeriods = combiner.getVariants();
expect(variantsAfter4Periods).toEqual(variantsAfterAllPeriods);
});
describe('compareClosestPreferLower', () => {
const PeriodCombiner = shaka.util.PeriodCombiner;
const {BETTER, EQUAL, WORSE} = shaka.util.PeriodCombiner.BetterOrWorse;
it('Prefers value equal to the output', () => {
let isCandidateBetter = PeriodCombiner.compareClosestPreferLower(
/* output= */ 5, /* bestValue= */ 5, /* candidateValue= */ 3);
expect(isCandidateBetter).toBe(WORSE);
// Make sure it works correctly whether it's the candidate or the best
// value that is equal to the output.
isCandidateBetter = PeriodCombiner.compareClosestPreferLower(
/* output= */ 5, /* bestValue= */ 3, /* candidateValue= */ 5);
expect(isCandidateBetter).toBe(BETTER);
});
it('Prefers a value lower than the output', () => {
let isCandidateBetter = PeriodCombiner.compareClosestPreferLower(
/* output= */ 5, /* bestValue= */ 3, /* candidateValue= */ 6);
expect(isCandidateBetter).toBe(WORSE);
isCandidateBetter = PeriodCombiner.compareClosestPreferLower(
/* output= */ 5, /* bestValue= */ 7, /* candidateValue= */ 2);
expect(isCandidateBetter).toBe(BETTER);
});
it('If both values are lower than the output,' +
' prefer the one that\'s closer', () => {
let isCandidateBetter = PeriodCombiner.compareClosestPreferLower(
/* output= */ 5, /* bestValue= */ 4, /* candidateValue= */ 3);
expect(isCandidateBetter).toBe(WORSE);
isCandidateBetter = PeriodCombiner.compareClosestPreferLower(
/* output= */ 5, /* bestValue= */ 2, /* candidateValue= */ 3);
expect(isCandidateBetter).toBe(BETTER);
});
it('If both values are greater than the output,' +
' prefer the one that\'s closer', () => {
let isCandidateBetter = PeriodCombiner.compareClosestPreferLower(
/* output= */ 5, /* bestValue= */ 6, /* candidateValue= */ 7);
expect(isCandidateBetter).toBe(WORSE);
isCandidateBetter = PeriodCombiner.compareClosestPreferLower(
/* output= */ 5, /* bestValue= */ 9, /* candidateValue= */ 8);
expect(isCandidateBetter).toBe(BETTER);
});
});
/** @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 ?
streamGenerator.language + '*' : streamGenerator.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 ?
streamGenerator.language + '*' : streamGenerator.language;
return streamGenerator.build_();
}
/**
* @param {number} height
* @return {shaka.extern.Stream}
* @suppress {accessControls}
*/
function makeImageStream(height) {
const width = height * 4 / 3;
const streamGenerator = new shaka.test.ManifestGenerator.Stream(
/* manifest= */ null,
/* isPartial= */ false,
/* id= */ nextId++,
/* type= */ shaka.util.ManifestParserUtils.ContentType.IMAGE);
streamGenerator.size(width, height);
streamGenerator.originalId = height.toString();
streamGenerator.mime('image/jpeg');
streamGenerator.tilesLayout = '1x1';
return streamGenerator.build_();
}
/**
* @param {number} height
* @param {string} language
* @param {number=} channels
* @return {shaka.extern.Variant}
*/
function makeAVVariant(height, language, channels = 2, roles = []) {
const variant = jasmine.objectContaining({
language,
audio: jasmine.objectContaining({
language,
roles,
channelsCount: channels,
}),
video: jasmine.objectContaining({
height,
}),
});
return /** @type {shaka.extern.Variant} */(/** @type {?} */(variant));
}
});