Files
shaka-player/test/ads/interstitial_ad_manager_unit.js
T
Álvaro Velad Galván dbfe560b66 fix(Ads): Fix send _HLS_primary_id on HLS interstitials (#9581)
See:
https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#appendix-D.4
In particular:
> Clients that cannot set the X-PLAYBACK-SESSION-ID request header
   SHOULD create a globally-unique value for every primary playback
   session, and provide this value as an _HLS_primary_id query parameter
   on both the request for the primary asset and interstitial requests
   made on behalf of that asset.
2026-01-21 12:27:53 +01:00

2456 lines
66 KiB
JavaScript

/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
describe('Interstitial Ad manager', () => {
const TXml = shaka.util.TXml;
/** @type {!shaka.test.FakeNetworkingEngine} */
let networkingEngine;
/** @type {!HTMLElement} */
let adContainer;
/** @type {!shaka.Player} */
let player;
/** @type {!HTMLVideoElement} */
let video;
/** @type {!jasmine.Spy} */
let onEventSpy;
/** @type {!shaka.ads.InterstitialAdManager} */
let interstitialAdManager;
beforeEach(async () => {
// Allows us to use a timer instead of requestVideoFrameCallback
// (which doesn't work well in all platform tests)
spyOn(deviceDetected, 'getDeviceType')
.and.returnValue(shaka.device.IDevice.DeviceType.TV);
function dependencyInjector(player) {
// Create a networking engine that always returns an empty buffer.
networkingEngine = new shaka.test.FakeNetworkingEngine();
networkingEngine.setDefaultValue(new ArrayBuffer(0));
player.createNetworkingEngine = () => networkingEngine;
}
adContainer =
/** @type {!HTMLElement} */ (document.createElement('div'));
player = new shaka.Player(null, null, dependencyInjector);
video = shaka.test.UiUtils.createVideoElement();
await player.attach(video);
onEventSpy = jasmine.createSpy('onEvent');
interstitialAdManager = new shaka.ads.InterstitialAdManager(
adContainer, player, shaka.test.Util.spyFunc(onEventSpy));
const config = shaka.util.PlayerConfiguration.createDefault().ads;
// We always support multiple video elements so that we can properly
// control timing in unit tests.
config.supportsMultipleMediaElements = true;
interstitialAdManager.configure(config);
});
afterEach(async () => {
interstitialAdManager.release();
await player.destroy();
});
describe('HLS', () => {
it('basic interstitial support', async () => {
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'PREROLL',
},
{
key: 'CUE',
data: 'PRE',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'X-RESUME-OFFSET',
data: '0.0',
},
{
key: 'X-RESTRICT',
data: 'SKIP,JUMP',
},
],
};
await interstitialAdManager.addMetadata(metadata);
expect(onEventSpy).toHaveBeenCalledTimes(2);
const eventValuePreload = {
type: 'ad-interstitial-preload',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValuePreload));
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 0,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
});
it('supports multiple interstitials', async () => {
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'PREROLL',
},
{
key: 'CUE',
data: 'PRE',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'X-RESUME-OFFSET',
data: '0.0',
},
{
key: 'X-RESTRICT',
data: 'SKIP,JUMP',
},
],
};
await interstitialAdManager.addMetadata(metadata);
const metadata2 = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'POSTROLL',
},
{
key: 'CUE',
data: 'POST',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'X-RESUME-OFFSET',
data: '0.0',
},
{
key: 'X-RESTRICT',
data: 'SKIP,JUMP',
},
],
};
await interstitialAdManager.addMetadata(metadata2);
expect(onEventSpy).toHaveBeenCalledTimes(4);
const eventValuePreload = {
type: 'ad-interstitial-preload',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValuePreload));
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 0,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
const eventValue2 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 0,
end: null,
},
{
start: -1,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue2));
});
it('ignore duplicate interstitials', async () => {
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'PREROLL',
},
{
key: 'CUE',
data: 'PRE',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'X-RESUME-OFFSET',
data: '0.0',
},
{
key: 'X-RESTRICT',
data: 'SKIP,JUMP',
},
],
};
await interstitialAdManager.addMetadata(metadata);
await interstitialAdManager.addMetadata(metadata);
expect(onEventSpy).toHaveBeenCalledTimes(2);
const eventValuePreload = {
type: 'ad-interstitial-preload',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValuePreload));
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 0,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
});
it('ignore invalid interstitial', async () => {
// It is not valid because it does not have an interstitial URL
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'PREROLL',
},
{
key: 'CUE',
data: 'PRE',
},
{
key: 'X-RESUME-OFFSET',
data: '0.0',
},
{
key: 'X-RESTRICT',
data: 'SKIP,JUMP',
},
],
};
await interstitialAdManager.addMetadata(metadata);
expect(onEventSpy).not.toHaveBeenCalled();
});
it('supports X-ASSET-LIST', async () => {
spyOn(window.crypto, 'randomUUID').and.returnValue('1');
const assetsList = JSON.stringify({
ASSETS: [
{
URI: 'ad.m3u8',
},
],
});
networkingEngine.setResponseText(
'test:/test.json?_HLS_primary_id=1', assetsList);
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'PREROLL',
},
{
key: 'CUE',
data: 'PRE',
},
{
key: 'X-ASSET-LIST',
data: 'test:/test.json',
},
{
key: 'X-RESUME-OFFSET',
data: '0.0',
},
{
key: 'X-RESTRICT',
data: 'SKIP,JUMP',
},
],
};
await interstitialAdManager.addMetadata(metadata);
expect(onEventSpy).toHaveBeenCalledTimes(2);
const eventValuePreload = {
type: 'ad-interstitial-preload',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValuePreload));
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 0,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
});
it('supports X-ASSET-LIST with X-AD-CREATIVE-SIGNALING', async () => {
spyOn(window.crypto, 'randomUUID').and.returnValue('1');
const assetsList = JSON.stringify({
ASSETS: [
{
'URI': 'ad.m3u8',
'X-AD-CREATIVE-SIGNALING': {
version: 2,
type: 'slot',
payload: [
{
type: 'linear',
start: 0,
duration: 8,
media: [],
identifiers: [],
tracking: [
{
type: 'impression',
urls: ['impression'],
},
{
type: 'clickTracking',
urls: ['clickTracking'],
},
{
type: 'start',
urls: ['start'],
},
{
type: 'firstQuartile',
urls: ['firstQuartile'],
},
{
type: 'firstQuartile',
urls: ['firstQuartile_alt'],
},
{
type: 'midpoint',
urls: ['midpoint', 'midpoint_alt'],
},
{
type: 'thirdQuartile',
urls: ['thirdQuartile'],
},
{
type: 'complete',
urls: ['complete'],
},
{
type: 'skip',
urls: ['skip'],
},
{
type: 'error',
urls: ['error'],
},
{
type: 'resume',
urls: ['resume'],
},
{
type: 'pause',
urls: ['pause'],
},
{
type: 'mute',
urls: ['mute'],
},
{
type: 'unmute',
urls: ['unmute'],
},
],
verifications: [],
clickThrough: 'clickThrough',
},
],
},
},
],
});
networkingEngine.setResponseText(
'test:/test.json?_HLS_primary_id=1', assetsList);
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'PREROLL',
},
{
key: 'CUE',
data: 'PRE',
},
{
key: 'X-ASSET-LIST',
data: 'test:/test.json',
},
{
key: 'X-RESUME-OFFSET',
data: '0.0',
},
{
key: 'X-RESTRICT',
data: 'SKIP,JUMP',
},
],
};
await interstitialAdManager.addMetadata(metadata);
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'PREROLL_shaka_asset_0',
groupId: 'PREROLL',
startTime: 0,
endTime: null,
uri: 'ad.m3u8?_HLS_primary_id=1',
mimeType: 'application/x-mpegurl',
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: false,
resumeOffset: 0,
playoutLimit: null,
once: false,
pre: true,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: 'clickThrough',
tracking: {
impression: ['impression'],
clickTracking: ['clickTracking'],
start: ['start'],
firstQuartile: ['firstQuartile', 'firstQuartile_alt'],
midpoint: ['midpoint', 'midpoint_alt'],
thirdQuartile: ['thirdQuartile'],
complete: ['complete'],
skip: ['skip'],
error: ['error'],
resume: ['resume'],
pause: ['pause'],
mute: ['mute'],
unmute: ['unmute'],
},
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('supports X-RESTRICT', async () => {
spyOn(window.crypto, 'randomUUID').and.returnValue('1');
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'TEST',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'X-RESTRICT',
data: 'SKIP,JUMP',
},
],
};
await interstitialAdManager.addMetadata(metadata);
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'TEST',
groupId: null,
startTime: 0,
endTime: null,
uri: 'test.m3u8?_HLS_primary_id=1',
mimeType: 'application/x-mpegurl',
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: false,
resumeOffset: null,
playoutLimit: null,
once: false,
pre: false,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('supports X-SKIP-CONTROL-OFFSET, X-SKIP-CONTROL-DURATION', async () => {
spyOn(window.crypto, 'randomUUID').and.returnValue('1');
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'TEST',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'X-SKIP-CONTROL-OFFSET',
data: 5,
},
{
key: 'X-SKIP-CONTROL-DURATION',
data: 10,
},
],
};
await interstitialAdManager.addMetadata(metadata);
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'TEST',
groupId: null,
startTime: 0,
endTime: null,
uri: 'test.m3u8?_HLS_primary_id=1',
mimeType: 'application/x-mpegurl',
isSkippable: true,
skipOffset: 5,
skipFor: 10,
canJump: true,
resumeOffset: null,
playoutLimit: null,
once: false,
pre: false,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('supports X-RESUME-OFFSET', async () => {
spyOn(window.crypto, 'randomUUID').and.returnValue('1');
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'TEST',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'X-RESUME-OFFSET',
data: '1.5',
},
],
};
await interstitialAdManager.addMetadata(metadata);
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'TEST',
groupId: null,
startTime: 0,
endTime: null,
uri: 'test.m3u8?_HLS_primary_id=1',
mimeType: 'application/x-mpegurl',
isSkippable: true,
skipOffset: 0,
skipFor: null,
canJump: true,
resumeOffset: 1.5,
playoutLimit: null,
once: false,
pre: false,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('supports X-PLAYOUT-LIMIT', async () => {
spyOn(window.crypto, 'randomUUID').and.returnValue('1');
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'TEST',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'X-PLAYOUT-LIMIT',
data: '15',
},
],
};
await interstitialAdManager.addMetadata(metadata);
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'TEST',
groupId: null,
startTime: 0,
endTime: null,
uri: 'test.m3u8?_HLS_primary_id=1',
mimeType: 'application/x-mpegurl',
isSkippable: true,
skipOffset: 0,
skipFor: null,
canJump: true,
resumeOffset: null,
playoutLimit: 15,
once: false,
pre: false,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('supports CUE-ONCE', async () => {
spyOn(window.crypto, 'randomUUID').and.returnValue('1');
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'TEST',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'CUE',
data: 'ONCE',
},
],
};
await interstitialAdManager.addMetadata(metadata);
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'TEST',
groupId: null,
startTime: 0,
endTime: null,
uri: 'test.m3u8?_HLS_primary_id=1',
mimeType: 'application/x-mpegurl',
isSkippable: true,
skipOffset: 0,
skipFor: null,
canJump: true,
resumeOffset: null,
playoutLimit: null,
once: true,
pre: false,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('supports CUE-PRE', async () => {
spyOn(window.crypto, 'randomUUID').and.returnValue('1');
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'TEST',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'CUE',
data: 'PRE',
},
],
};
await interstitialAdManager.addMetadata(metadata);
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'TEST',
groupId: null,
startTime: 0,
endTime: null,
uri: 'test.m3u8?_HLS_primary_id=1',
mimeType: 'application/x-mpegurl',
isSkippable: true,
skipOffset: 0,
skipFor: null,
canJump: true,
resumeOffset: null,
playoutLimit: null,
once: false,
pre: true,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('supports CUE-POST', async () => {
spyOn(window.crypto, 'randomUUID').and.returnValue('1');
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'TEST',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'CUE',
data: 'POST',
},
],
};
await interstitialAdManager.addMetadata(metadata);
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'TEST',
groupId: null,
startTime: 0,
endTime: null,
uri: 'test.m3u8?_HLS_primary_id=1',
mimeType: 'application/x-mpegurl',
isSkippable: true,
skipOffset: 0,
skipFor: null,
canJump: true,
resumeOffset: null,
playoutLimit: null,
once: false,
pre: false,
post: true,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('supports X-TIMELINE-OCCUPIES', async () => {
spyOn(window.crypto, 'randomUUID').and.returnValue('1');
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 100,
endTime: 130,
values: [
{
key: 'ID',
data: 'TEST',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'X-TIMELINE-OCCUPIES',
data: 'RANGE',
},
],
};
await interstitialAdManager.addMetadata(metadata);
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'TEST',
groupId: null,
startTime: 100,
endTime: 130,
uri: 'test.m3u8?_HLS_primary_id=1',
mimeType: 'application/x-mpegurl',
isSkippable: true,
skipOffset: 0,
skipFor: null,
canJump: true,
resumeOffset: null,
playoutLimit: null,
once: false,
pre: false,
post: false,
timelineRange: true,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('supports SKIP-CONTROL OFFSET and DURATION', async () => {
spyOn(window.crypto, 'randomUUID').and.returnValue('1');
const assetsList = JSON.stringify({
'ASSETS': [
{
URI: 'ad.m3u8',
},
],
'SKIP-CONTROL': {
OFFSET: 5,
DURATION: 10,
},
});
networkingEngine.setResponseText(
'test:/test.json?_HLS_primary_id=1', assetsList);
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'TEST',
},
{
key: 'X-ASSET-LIST',
data: 'test:/test.json',
},
],
};
await interstitialAdManager.addMetadata(metadata);
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'TEST_shaka_asset_0',
groupId: 'TEST',
startTime: 0,
endTime: null,
uri: 'ad.m3u8?_HLS_primary_id=1',
mimeType: 'application/x-mpegurl',
isSkippable: true,
skipOffset: 5,
skipFor: 10,
canJump: true,
resumeOffset: null,
playoutLimit: null,
once: false,
pre: false,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('supports overlay events with L-Shape format', async () => {
spyOn(window.crypto, 'randomUUID').and.returnValue('1');
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: 1,
values: [
{
key: 'X-OVERLAY-ID',
data: 'OVERLAY',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'X-ASSET-MIMETYPE',
data: 'application/vnd.apple.mpegurl',
},
{
key: 'X-DEPTH',
data: '-1',
},
{
key: 'X-LOOP',
data: 'NO',
},
{
key: 'X-VIEWPORT',
data: '1920x1080',
},
{
key: 'X-OVERLAY-SIZE',
data: '1920x1080',
},
{
key: 'X-OVERLAY-POSITION',
data: '0x0',
},
{
key: 'X-SQUEEZECURRENT',
data: '0.5',
},
],
};
await interstitialAdManager.addMetadata(metadata);
expect(onEventSpy).not.toHaveBeenCalled();
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'OVERLAY',
groupId: null,
startTime: 0,
endTime: 1,
uri: 'test.m3u8?_HLS_primary_id=1',
mimeType: 'application/vnd.apple.mpegurl',
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: true,
resumeOffset: null,
playoutLimit: null,
once: false,
pre: false,
post: false,
timelineRange: true,
loop: false,
overlay: {
viewport: {
x: 1920,
y: 1080,
},
topLeft: {
x: 0,
y: 0,
},
size: {
x: 1920,
y: 1080,
},
},
displayOnBackground: true,
currentVideo: {
viewport: {
x: 1920,
y: 1080,
},
topLeft: {
x: 0,
y: 0,
},
size: {
x: 960,
y: 540,
},
},
background: null,
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('supports overlay events double box format', async () => {
spyOn(window.crypto, 'randomUUID').and.returnValue('1');
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: 1,
values: [
{
key: 'X-OVERLAY-ID',
data: 'OVERLAY',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'X-ASSET-MIMETYPE',
data: 'application/vnd.apple.mpegurl',
},
{
key: 'X-DEPTH',
data: '-1',
},
{
key: 'X-LOOP',
data: 'NO',
},
{
key: 'X-VIEWPORT',
data: '1920x1080',
},
{
key: 'X-OVERLAY-SIZE',
data: '864x486',
},
{
key: 'X-OVERLAY-POSITION',
data: '864x297',
},
{
key: 'X-SQUEEZECURRENT',
data: '0.3',
},
{
key: 'X-SQUEEZECURRENT-POSITION',
data: '192x378',
},
{
key: 'X-BACKGROUND',
data: 'red',
},
],
};
await interstitialAdManager.addMetadata(metadata);
expect(onEventSpy).not.toHaveBeenCalled();
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'OVERLAY',
groupId: null,
startTime: 0,
endTime: 1,
uri: 'test.m3u8?_HLS_primary_id=1',
mimeType: 'application/vnd.apple.mpegurl',
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: true,
resumeOffset: null,
playoutLimit: null,
once: false,
pre: false,
post: false,
timelineRange: true,
loop: false,
overlay: {
viewport: {
x: 1920,
y: 1080,
},
topLeft: {
x: 864,
y: 297,
},
size: {
x: 864,
y: 486,
},
},
displayOnBackground: true,
currentVideo: {
viewport: {
x: 1920,
y: 1080,
},
topLeft: {
x: 192,
y: 378,
},
size: {
x: 576,
y: 324,
},
},
background: 'red',
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
});
describe('DASH', () => {
it('supports alternative MPD', async () => {
const eventString = [
'<Event duration="1" id="PREROLL" presentationTime="0">',
'<InsertPresentation url="test.mpd"/>',
'</Event>',
].join('');
const eventNode = TXml.parseXmlString(eventString);
goog.asserts.assert(eventNode, 'Should have a event node!');
/** @type {shaka.extern.TimelineRegionInfo} */
const region = {
startTime: 0,
endTime: 1,
id: 'PREROLL',
schemeIdUri: 'urn:mpeg:dash:event:alternativeMPD:insert:2025',
eventNode,
value: '',
timescale: 1,
};
await interstitialAdManager.addRegion(region);
expect(onEventSpy).toHaveBeenCalledTimes(1);
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 0,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
});
it('supports alternative MPD with noJump and skipAfter', async () => {
const eventString = [
'<Event duration="1" id="TEST" presentationTime="1">',
'<InsertPresentation url="test.mpd" noJump="1" skipAfter="PT0S"/>',
'</Event>',
].join('');
const eventNode = TXml.parseXmlString(eventString);
goog.asserts.assert(eventNode, 'Should have a event node!');
/** @type {shaka.extern.TimelineRegionInfo} */
const region = {
startTime: 1,
endTime: 2,
id: 'TEST',
schemeIdUri: 'urn:mpeg:dash:event:alternativeMPD:insert:2025',
eventNode,
value: '',
timescale: 1,
};
await interstitialAdManager.addRegion(region);
expect(onEventSpy).toHaveBeenCalledTimes(1);
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 1,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'TEST',
groupId: null,
startTime: 1,
endTime: 2,
uri: 'test.mpd',
mimeType: 'application/dash+xml',
isSkippable: true,
skipOffset: 0,
skipFor: null,
canJump: false,
resumeOffset: 0,
playoutLimit: null,
once: false,
pre: false,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('ignore duplicate alternative MPD', async () => {
const eventString = [
'<Event duration="1" id="PREROLL" presentationTime="0">',
'<ReplacePresentation url="test.mpd" returnOffset="1"/>',
'</Event>',
].join('');
const eventNode = TXml.parseXmlString(eventString);
goog.asserts.assert(eventNode, 'Should have a event node!');
/** @type {shaka.extern.TimelineRegionInfo} */
const region = {
startTime: 0,
endTime: 1,
id: 'PREROLL',
schemeIdUri: 'urn:mpeg:dash:event:alternativeMPD:replace:2025',
eventNode,
value: '',
timescale: 1,
};
await interstitialAdManager.addRegion(region);
await interstitialAdManager.addRegion(region);
expect(onEventSpy).toHaveBeenCalledTimes(1);
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 0,
end: 1,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
});
it('ignore invalid alternative MPD', async () => {
// It is not valid because it does not have an interstitial URL
const eventString = [
'<Event duration="1" id="PREROLL" presentationTime="0">',
'<InsertPresentation/>',
'</Event>',
].join('');
const eventNode = TXml.parseXmlString(eventString);
goog.asserts.assert(eventNode, 'Should have a event node!');
/** @type {shaka.extern.TimelineRegionInfo} */
const region = {
startTime: 0,
endTime: 1,
id: 'PREROLL',
schemeIdUri: 'urn:mpeg:dash:event:alternativeMPD:insert:2025',
eventNode,
value: '',
timescale: 1,
};
await interstitialAdManager.addRegion(region);
expect(onEventSpy).not.toHaveBeenCalled();
});
it('supports overlay events', async () => {
const eventString = [
'<Event duration="1" id="OVERLAY" presentationTime="0">',
'<OverlayEvent mimeType="application/dash+xml" uri="test.mpd">',
'<Viewport x="1920" y="1080"/>',
'<Overlay>',
'<TopLeft x="0" y="720"/>',
'<Size x="480" y="360"/>',
'</Overlay>',
'</OverlayEvent>',
'</Event>',
].join('');
const eventNode = TXml.parseXmlString(eventString);
goog.asserts.assert(eventNode, 'Should have a event node!');
/** @type {shaka.extern.TimelineRegionInfo} */
const region = {
startTime: 0,
endTime: 1,
id: 'OVERLAY',
schemeIdUri: 'urn:mpeg:dash:event:2012',
eventNode,
value: '',
timescale: 1,
};
await interstitialAdManager.addOverlayRegion(region);
expect(onEventSpy).not.toHaveBeenCalled();
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'OVERLAY',
groupId: null,
startTime: 0,
endTime: 1,
uri: 'test.mpd',
mimeType: 'application/dash+xml',
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: true,
resumeOffset: null,
playoutLimit: null,
once: false,
pre: false,
post: false,
timelineRange: true,
loop: false,
overlay: {
viewport: {
x: 1920,
y: 1080,
},
topLeft: {
x: 0,
y: 720,
},
size: {
x: 480,
y: 360,
},
},
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('ignore overlay events with z=0', async () => {
const eventString = [
'<Event duration="1" id="OVERLAY" presentationTime="0">',
'<OverlayEvent mimeType="application/dash+xml" uri="test.mpd" z="0">',
'<Viewport x="1920" y="1080"/>',
'<Squeeze>',
'<TopLeft x="0" y="720"/>',
'<Size x="480" y="360"/>',
'</Squeeze>',
'</OverlayEvent>',
'</Event>',
].join('');
const eventNode = TXml.parseXmlString(eventString);
goog.asserts.assert(eventNode, 'Should have a event node!');
/** @type {shaka.extern.TimelineRegionInfo} */
const region = {
startTime: 0,
endTime: 1,
id: 'OVERLAY',
schemeIdUri: 'urn:mpeg:dash:event:2012',
eventNode,
value: '',
timescale: 1,
};
await interstitialAdManager.addOverlayRegion(region);
expect(onEventSpy).not.toHaveBeenCalled();
});
it('supports overlay events with L-Shape format', async () => {
const eventString = [
'<Event duration="1" id="OVERLAY" presentationTime="0">',
'<OverlayEvent mimeType="application/dash+xml" uri="test.mpd" z="-1">',
'<Viewport x="1920" y="1080"/>',
'<Squeeze>',
'<TopLeft x="0" y="0"/>',
'<Size x="960" y="540"/>',
'</Squeeze>',
'</OverlayEvent>',
'</Event>',
].join('');
const eventNode = TXml.parseXmlString(eventString);
goog.asserts.assert(eventNode, 'Should have a event node!');
/** @type {shaka.extern.TimelineRegionInfo} */
const region = {
startTime: 0,
endTime: 1,
id: 'OVERLAY',
schemeIdUri: 'urn:scte:dash:scte214-events',
eventNode,
value: '',
timescale: 1,
};
await interstitialAdManager.addOverlayRegion(region);
expect(onEventSpy).not.toHaveBeenCalled();
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'OVERLAY',
groupId: null,
startTime: 0,
endTime: 1,
uri: 'test.mpd',
mimeType: 'application/dash+xml',
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: true,
resumeOffset: null,
playoutLimit: null,
once: false,
pre: false,
post: false,
timelineRange: true,
loop: false,
overlay: {
viewport: {
x: 1920,
y: 1080,
},
topLeft: {
x: 0,
y: 0,
},
size: {
x: 1920,
y: 1080,
},
},
displayOnBackground: true,
currentVideo: {
viewport: {
x: 1920,
y: 1080,
},
topLeft: {
x: 0,
y: 0,
},
size: {
x: 960,
y: 540,
},
},
background: null,
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('supports overlay events with double box format', async () => {
const eventString = [
'<Event duration="1" id="OVERLAY" presentationTime="0">',
'<OverlayEvent mimeType="application/dash+xml" uri="test.mpd" z="-1">',
'<Background>red</Background>',
'<Viewport x="1920" y="1080"/>',
'<Overlay>',
'<TopLeft x="0" y="720"/>',
'<Size x="480" y="360"/>',
'</Overlay>',
'<Squeeze>',
'<TopLeft x="0" y="0"/>',
'<Size x="960" y="540"/>',
'</Squeeze>',
'</OverlayEvent>',
'</Event>',
].join('');
const eventNode = TXml.parseXmlString(eventString);
goog.asserts.assert(eventNode, 'Should have a event node!');
/** @type {shaka.extern.TimelineRegionInfo} */
const region = {
startTime: 0,
endTime: 1,
id: 'OVERLAY',
schemeIdUri: 'urn:scte:dash:scte214-events',
eventNode,
value: '',
timescale: 1,
};
await interstitialAdManager.addOverlayRegion(region);
expect(onEventSpy).not.toHaveBeenCalled();
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: 'OVERLAY',
groupId: null,
startTime: 0,
endTime: 1,
uri: 'test.mpd',
mimeType: 'application/dash+xml',
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: true,
resumeOffset: null,
playoutLimit: null,
once: false,
pre: false,
post: false,
timelineRange: true,
loop: false,
overlay: {
viewport: {
x: 1920,
y: 1080,
},
topLeft: {
x: 0,
y: 720,
},
size: {
x: 480,
y: 360,
},
},
displayOnBackground: true,
currentVideo: {
viewport: {
x: 1920,
y: 1080,
},
topLeft: {
x: 0,
y: 0,
},
size: {
x: 960,
y: 540,
},
},
background: 'red',
clickThroughUrl: null,
tracking: null,
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
});
describe('custom', () => {
it('basic interstitial support', async () => {
/** @type {!shaka.extern.AdInterstitial} */
const interstitial = {
id: null,
groupId: null,
startTime: 10,
endTime: null,
uri: 'test.mp4',
mimeType: null,
isSkippable: true,
skipOffset: 10,
skipFor: null,
canJump: false,
resumeOffset: null,
playoutLimit: null,
once: true,
pre: false,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
await interstitialAdManager.addInterstitials([interstitial]);
expect(onEventSpy).toHaveBeenCalledTimes(1);
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 10,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
});
it('supports multiple interstitials', async () => {
/** @type {!Array<!shaka.extern.AdInterstitial>} */
const interstitials = [
{
id: null,
groupId: null,
startTime: 0,
endTime: null,
uri: 'test.mp4',
mimeType: null,
isSkippable: true,
skipOffset: 5,
skipFor: null,
canJump: false,
resumeOffset: null,
playoutLimit: null,
once: true,
pre: false,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
},
{
id: null,
groupId: null,
startTime: 10,
endTime: null,
uri: 'test.mp4',
mimeType: null,
isSkippable: true,
skipOffset: 10,
skipFor: null,
canJump: false,
resumeOffset: null,
playoutLimit: null,
once: true,
pre: false,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
},
];
await interstitialAdManager.addInterstitials(interstitials);
expect(onEventSpy).toHaveBeenCalledTimes(2);
const eventValuePreload = {
type: 'ad-interstitial-preload',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValuePreload));
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 0,
end: null,
},
{
start: 10,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
});
it('ignore duplicate interstitial', async () => {
/** @type {!shaka.extern.AdInterstitial} */
const interstitial = {
id: null,
groupId: null,
startTime: 10,
endTime: null,
uri: 'test.mp4',
mimeType: null,
isSkippable: true,
skipOffset: 10,
skipFor: null,
canJump: false,
resumeOffset: null,
playoutLimit: null,
once: true,
pre: false,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
const interstitials = [interstitial, interstitial];
await interstitialAdManager.addInterstitials(interstitials);
expect(onEventSpy).toHaveBeenCalledTimes(1);
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 10,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
});
it('ignore invalid interstitial', async () => {
// It is not valid because it does not have an interstitial URL
/** @type {!shaka.extern.AdInterstitial} */
const interstitial = {
id: null,
groupId: null,
startTime: 10,
endTime: null,
uri: '',
mimeType: null,
isSkippable: true,
skipOffset: 10,
skipFor: null,
canJump: false,
resumeOffset: null,
playoutLimit: null,
once: true,
pre: false,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
await interstitialAdManager.addInterstitials([interstitial]);
expect(onEventSpy).not.toHaveBeenCalled();
});
});
describe('VAST', () => {
it('basic interstitial support', async () => {
const vast = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<VAST version="3.0">',
'<Ad id="5925573263">',
'<InLine>',
'<Error>error_url</Error>',
'<Error>error_url2</Error>',
'<Impression>impression_url</Impression>',
'<Impression>impression_url2</Impression>',
'<Creatives>',
'<Creative id="138381721867" sequence="1">',
'<Linear>',
'<Duration>00:00:10</Duration>',
'<TrackingEvents>',
'<Tracking event="start">start_url</Tracking>',
'<Tracking event="start">start_url2</Tracking>',
'<Tracking event="firstQuartile">firstQuartile_url</Tracking>',
'<Tracking event="midpoint">midpoint_url</Tracking>',
'<Tracking event="thirdQuartile">thirdQuartile_url</Tracking>',
'<Tracking event="complete">complete_url</Tracking>',
'<Tracking event="skip">skip_url</Tracking>',
'<Tracking event="resume">resume_url</Tracking>',
'<Tracking event="pause">pause_url</Tracking>',
'<Tracking event="mute">mute_url</Tracking>',
'<Tracking event="unmute">unmute_url</Tracking>',
'</TrackingEvents>',
'<VideoClicks>',
'<ClickThrough id="1">',
'<![CDATA[ foo.bar ]]>',
'</ClickThrough>',
'<ClickTracking id="1">click_tracking</ClickTracking>',
'</VideoClicks>',
'<MediaFiles>',
'<MediaFile bitrate="140" delivery="progressive" ',
'height="360" type="video/mp4" width="640">',
'<![CDATA[test.mp4]]>',
'</MediaFile>',
'</MediaFiles>',
'</Linear>',
'</Creative>',
'</Creatives>',
'</InLine>',
'</Ad>',
'</VAST>',
].join('');
networkingEngine.setResponseText('test:/vast', vast);
await interstitialAdManager.addAdUrlInterstitial('test:/vast');
expect(onEventSpy).toHaveBeenCalledTimes(2);
const eventValuePreload = {
type: 'ad-interstitial-preload',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValuePreload));
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 0,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: '5925573263',
groupId: null,
startTime: 0,
endTime: null,
uri: 'test.mp4',
mimeType: 'video/mp4',
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: false,
resumeOffset: 0,
playoutLimit: null,
once: true,
pre: true,
post: false,
timelineRange: false,
loop: false,
overlay: null,
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: 'foo.bar',
tracking: {
impression: ['impression_url', 'impression_url2'],
clickTracking: ['click_tracking'],
start: ['start_url', 'start_url2'],
firstQuartile: ['firstQuartile_url'],
midpoint: ['midpoint_url'],
thirdQuartile: ['thirdQuartile_url'],
complete: ['complete_url'],
skip: ['skip_url'],
error: ['error_url', 'error_url2'],
resume: ['resume_url'],
pause: ['pause_url'],
mute: ['mute_url'],
unmute: ['unmute_url'],
},
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('supports non-linear ads', async () => {
const vast = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<VAST version="3.0">',
'<Ad id="5925573263">',
'<InLine>',
'<Error>error_url</Error>',
'<Impression>impression_url</Impression>',
'<Creatives>',
'<Creative id="138381721867" sequence="1">',
'<NonLinearAds>',
'<TrackingEvents>',
'<Tracking event="start">start_url</Tracking>',
'<Tracking event="firstQuartile">firstQuartile_url</Tracking>',
'<Tracking event="midpoint">midpoint_url</Tracking>',
'<Tracking event="thirdQuartile">thirdQuartile_url</Tracking>',
'<Tracking event="complete">complete_url</Tracking>',
'<Tracking event="skip">skip_url</Tracking>',
'<Tracking event="resume">resume_url</Tracking>',
'<Tracking event="pause">pause_url</Tracking>',
'<Tracking event="mute">mute_url</Tracking>',
'<Tracking event="unmute">unmute_url</Tracking>',
'</TrackingEvents>',
'<NonLinear width="535" height="80" minSuggestedDuration="00:00:05">',
'<StaticResource creativeType="image/png">',
'<![CDATA[test.png]]>',
'</StaticResource>',
'<NonLinearClickThrough>',
'<![CDATA[foo.bar]]>',
'</NonLinearClickThrough>',
'<NonLinearClickTracking>click_tracking</NonLinearClickTracking>',
'</NonLinear>',
'</NonLinearAds>',
'</Creative>',
'</Creatives>',
'</InLine>',
'</Ad>',
'</VAST>',
].join('');
networkingEngine.setResponseText('test:/vast', vast);
await interstitialAdManager.addAdUrlInterstitial('test:/vast');
expect(onEventSpy).not.toHaveBeenCalled();
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
/** @type {!shaka.extern.AdInterstitial} */
const expectedInterstitial = {
id: '5925573263',
groupId: null,
startTime: 0,
endTime: null,
uri: 'test.png',
mimeType: 'image/png',
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: false,
resumeOffset: 0,
playoutLimit: 5,
once: true,
pre: true,
post: false,
timelineRange: false,
loop: false,
overlay: {
viewport: {
x: 0,
y: 0,
},
topLeft: {
x: 0,
y: 0,
},
size: {
x: 535,
y: 80,
},
},
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: 'foo.bar',
tracking: {
impression: ['impression_url'],
clickTracking: ['click_tracking'],
start: ['start_url'],
firstQuartile: ['firstQuartile_url'],
midpoint: ['midpoint_url'],
thirdQuartile: ['thirdQuartile_url'],
complete: ['complete_url'],
skip: ['skip_url'],
error: ['error_url'],
resume: ['resume_url'],
pause: ['pause_url'],
mute: ['mute_url'],
unmute: ['unmute_url'],
},
};
expect(interstitials[0]).toEqual(expectedInterstitial);
});
it('ignore empty', async () => {
const vast = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<VAST version="3.0">',
'</VAST>',
].join('');
networkingEngine.setResponseText('test:/vast', vast);
await interstitialAdManager.addAdUrlInterstitial('test:/vast');
expect(onEventSpy).not.toHaveBeenCalled();
});
});
describe('VMAP', () => {
it('basic interstitial support', async () => {
const vmap = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<vmap:VMAP xmlns:vmap="http://www.iab.net/videosuite/vmap"',
' version="1.0">',
'<vmap:AdBreak timeOffset="start" breakType="linear"',
' breakId="midroll-1">',
'<vmap:AdSource id="preroll-ad-1" allowMultipleAds="false"',
' followRedirects="true">',
'<vmap:AdTagURI templateType="vast3"><![CDATA[test:/vast]]>',
'</vmap:AdTagURI>',
'</vmap:AdSource>',
'</vmap:AdBreak>',
'<vmap:AdBreak timeOffset="00:00:15.000" breakType="linear"',
' breakId="midroll-1">',
'<vmap:AdSource id="midroll-1-ad-1" allowMultipleAds="false"',
' followRedirects="true">',
'<vmap:AdTagURI templateType="vast3"><![CDATA[test:/vast]]>',
'</vmap:AdTagURI>',
'</vmap:AdSource>',
'</vmap:AdBreak>',
'<vmap:AdBreak timeOffset="end" breakType="linear"',
' breakId="midroll-1">',
'<vmap:AdSource id="postroll-ad-1" allowMultipleAds="false"',
' followRedirects="true">',
'<vmap:AdTagURI templateType="vast3"><![CDATA[test:/vast]]>',
'</vmap:AdTagURI>',
'</vmap:AdSource>',
'</vmap:AdBreak>',
'</vmap:VMAP>',
].join('');
const vast = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<VAST version="3.0">',
'<Ad>',
'<InLine>',
'<Creatives>',
'<Creative id="138381721867" sequence="1">',
'<Linear>',
'<Duration>00:00:10</Duration>',
'<MediaFiles>',
'<MediaFile bitrate="140" delivery="progressive" ',
'height="360" type="video/mp4" width="640">',
'<![CDATA[test.mp4]]>',
'</MediaFile>',
'</MediaFiles>',
'</Linear>',
'</Creative>',
'</Creatives>',
'</InLine>',
'</Ad>',
'</VAST>',
].join('');
networkingEngine
.setResponseText('test:/vmap', vmap)
.setResponseText('test:/vast', vast);
await interstitialAdManager.addAdUrlInterstitial('test:/vmap');
expect(onEventSpy).toHaveBeenCalledTimes(2);
const eventValuePreload = {
type: 'ad-interstitial-preload',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValuePreload));
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 0,
end: null,
},
{
start: 15,
end: null,
},
{
start: -1,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
});
it('ignore empty', async () => {
const vmap = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<vmap:VMAP xmlns:vmap="http://www.iab.net/videosuite/vmap"',
' version="1.0">',
'</vmap:VMAP>',
].join('');
networkingEngine.setResponseText('test:/vmap', vmap);
await interstitialAdManager.addAdUrlInterstitial('test:/vmap');
expect(onEventSpy).not.toHaveBeenCalled();
});
});
it('plays pre-roll correctly', async () => {
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'PREROLL',
},
{
key: 'CUE',
data: 'PRE',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'X-RESTRICT',
data: 'SKIP,JUMP',
},
],
};
await interstitialAdManager.addMetadata(metadata);
video.play();
video.dispatchEvent(new Event('timeupdate'));
await shaka.test.Util.shortDelay();
expect(onEventSpy).toHaveBeenCalledTimes(5);
const eventValuePreload = {
type: 'ad-interstitial-preload',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValuePreload));
const eventValueAdBreakStarted = {
type: 'ad-break-started',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValueAdBreakStarted));
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 0,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
const eventValue2 = {
type: 'ad-impression',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue2));
const eventValue3 = {
type: 'ad-started',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue3));
});
it('dispatch skip event correctly', async () => {
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 0,
endTime: null,
values: [
{
key: 'ID',
data: 'PREROLL',
},
{
key: 'CUE',
data: 'PRE',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
],
};
await interstitialAdManager.addMetadata(metadata);
video.play();
video.dispatchEvent(new Event('timeupdate'));
await shaka.test.Util.shortDelay();
expect(onEventSpy).toHaveBeenCalledTimes(6);
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 0,
end: null,
},
],
};
const eventValuePreload = {
type: 'ad-interstitial-preload',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValuePreload));
const eventValueAdBreakStarted = {
type: 'ad-break-started',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValueAdBreakStarted));
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
const eventValue2 = {
type: 'ad-impression',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue2));
const eventValue3 = {
type: 'ad-started',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue3));
const eventValue4 = {
type: 'ad-skip-state-changed',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue4));
});
it('jumping a mid-roll with JUMP restriction is not allowed', async () => {
const metadata = {
type: 'com.apple.quicktime.HLS',
startTime: 10,
endTime: null,
values: [
{
key: 'ID',
data: 'MIDROLL',
},
{
key: 'X-ASSET-URI',
data: 'test.m3u8',
},
{
key: 'X-RESTRICT',
data: 'SKIP,JUMP',
},
],
};
await interstitialAdManager.addMetadata(metadata);
video.currentTime = 0;
video.play();
video.dispatchEvent(new Event('timeupdate'));
await shaka.test.Util.shortDelay();
expect(onEventSpy).toHaveBeenCalledTimes(1);
const eventValue1 = {
type: 'ad-cue-points-changed',
cuepoints: [
{
start: 10,
end: null,
},
],
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue1));
video.currentTime = 20;
video.dispatchEvent(new Event('timeupdate'));
await shaka.test.Util.delay(0.25);
if (video.currentTime == 20) {
expect(onEventSpy).toHaveBeenCalledTimes(4);
const eventValueAdBreakStarted = {
type: 'ad-break-started',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValueAdBreakStarted));
const eventValue2 = {
type: 'ad-impression',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue2));
const eventValue3 = {
type: 'ad-started',
};
expect(onEventSpy).toHaveBeenCalledWith(
jasmine.objectContaining(eventValue3));
}
});
it('don\'t dispatch cue points changed if it is an overlay', async () => {
/** @type {!shaka.extern.AdInterstitial} */
const interstitial = {
id: null,
groupId: null,
startTime: 10,
endTime: null,
uri: 'test.mp4',
mimeType: null,
isSkippable: false,
skipOffset: null,
skipFor: null,
canJump: false,
resumeOffset: null,
playoutLimit: null,
once: true,
pre: false,
post: false,
timelineRange: false,
loop: false,
overlay: {
viewport: {
x: 1920,
y: 1080,
},
topLeft: {
x: 960,
y: 0,
},
size: {
x: 960,
y: 540,
},
},
displayOnBackground: false,
currentVideo: null,
background: null,
clickThroughUrl: null,
tracking: null,
};
await interstitialAdManager.addInterstitials([interstitial]);
expect(onEventSpy).not.toHaveBeenCalled();
const interstitials = interstitialAdManager.getInterstitials();
expect(interstitials.length).toBe(1);
});
});