shaka-player
Version:
DASH/EME video player library
777 lines (701 loc) • 24.1 kB
JavaScript
/** @license
* 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');
});
/** @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));
}
});