├── .babelrc
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── .jshintrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config
├── base.config.mjs
├── rollup.config.min.mjs
└── rollup.config.mjs
├── examples
├── advanced
│ ├── ads.js
│ └── index.html
├── autoplay
│ ├── ads.js
│ └── index.html
├── hls
│ ├── ads.js
│ └── index.html
├── manualplay
│ ├── ads.js
│ └── index.html
├── multiple
│ ├── ads.js
│ └── index.html
├── playlist
│ ├── ads.js
│ ├── img
│ │ ├── bbb_preview.jpg
│ │ └── stock_preview.jpg
│ └── index.html
├── reactjs
│ ├── ads.js
│ └── index.html
├── simple
│ ├── ads.js
│ └── index.html
└── style.css
├── index.html
├── package-lock.json
├── package.json
├── src
├── css
│ └── videojs.ima.css
├── ima-player.js
├── ima-plugin.js
├── ima-skip-button.js
├── ima-tech.js
└── ima-time-display.js
└── test
└── webdriver
├── basic.mjs
├── content
└── ads.js
└── index.html
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", {
4 | "debug": true,
5 | "targets": {
6 | "browsers": [
7 | ">0.5%",
8 | "not dead",
9 | "not op_mini all"
10 | ]
11 | }
12 | }]
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 |
7 | # Matches multiple files with brace expansion notation
8 | [*.{js,jsx,html,sass}]
9 | charset = utf-8
10 | indent_style = tab
11 | indent_size = 4
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'env': {
3 | 'browser': true,
4 | 'es6': true,
5 | },
6 | 'extends': ['eslint:recommended', 'google'],
7 | 'parserOptions': {
8 | 'sourceType': 'module',
9 | },
10 | 'rules': {
11 | 'jsdoc/check-types': 'error',
12 | 'space-infix-ops': 'error',
13 | 'no-console': ['error', {
14 | 'allow': ['warn', 'error']
15 | }],
16 | },
17 | 'plugins': [
18 | 'jsdoc',
19 | ],
20 | };
21 |
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esversion": 6
3 | }
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.6.6
2 |
3 | - fixed forceSkip method for simple VAST, updated several libs, tests etc
4 |
5 | ## 0.6.5
6 |
7 | - fixed videojs/browser bug where sometimes value of currentTime() is greater than duration()
8 |
9 | ## 0.6.4
10 |
11 | - settings video src directly on IOS handled by player, fixes content HLS errors
12 |
13 | ## 0.6.3
14 |
15 | - fixed vulnerabs, better logging of ima errors
16 |
17 | ## 0.6.2
18 |
19 | - force skip button
20 |
21 | ## 0.6.1
22 |
23 | - updated deps, fixed minor undefined error
24 |
25 | ## 0.5.6
26 |
27 | - better handling of late init
28 |
29 | ## 0.5.5
30 |
31 | - fixed unresponsive volume ad controls with moatwrapper
32 |
33 | ## 0.5.4
34 |
35 | - fixed another mute/volume mess
36 |
37 | ## 0.5.3
38 |
39 | - fixed volume/mute bug after IMA SDK new release
40 | - updated some packages
41 |
42 | ## 0.5.2
43 |
44 | - removed poster to reduce size
45 |
46 | ## 0.5.1
47 |
48 | - hotfix to prevent resume if content not started
49 |
50 | ## 0.5.0
51 |
52 | - fixed resume after skippable on ios
53 |
54 | ## 0.4.9
55 |
56 | - fixed timeout for preroll/postroll if no ads
57 |
58 | ## 0.4.8
59 |
60 | - fixed vulnerabilities and tests
61 |
62 | ## 0.4.7
63 |
64 | - check already initialized contrib-ads
65 |
66 | ## 0.4.6
67 |
68 | - npm publish
69 |
70 | ## 0.4.5
71 |
72 | - fixed vulnerability
73 | - contentchanged -> contentupdate
74 | - removed always true condition
75 |
76 | ## 0.4.4
77 |
78 | - dropped videojs v5 support
79 | - added support from v6.0.1 (v6.0.0 does not use setSource by mw)
80 | - fixed mute icon
81 |
82 | ## 0.4.3
83 |
84 | - another resize fixes
85 |
86 | ## 0.4.2
87 |
88 | - fixed ima triggering resize
89 |
90 | ## 0.4.1
91 |
92 | - fixed vulnerabilities
93 |
94 | ## 0.4.0
95 |
96 | - improved bundling
97 | - added examples to npm
98 | - added global videojs import
99 |
100 | ## 0.3.9
101 |
102 | - removed prerollScheduled as it broke unscheduled ads
103 |
104 | ## 0.3.8
105 |
106 | - fixed resize handling
107 |
108 | ## 0.3.7
109 |
110 | - videojs up to v7
111 | - upgraded geckodriver, webdriver, etc
112 | - added playbackRate tech method
113 | - more tests, fixed too fast webdriver click by ready event
114 |
115 | ## 0.3.6
116 |
117 | - initial resize bound to content player
118 | - added ima volume events to handle bar properly
119 | - changed command order of start/end linear ad to fix undefined content player
120 |
121 | ## 0.3.5
122 |
123 | - fixed initial dimensions
124 | - fixed ima corner case where currentAd is not defined
125 |
126 | ## 0.3.4
127 |
128 | - fixed timeout default value
129 |
130 | ## 0.3.3
131 |
132 | - sorted timeouts
133 | - README about timeouts
134 |
135 | ## 0.3.2
136 |
137 | - autoplay and muted state passed to IMA SDK
138 | - added autoplay example
139 |
140 | ## 0.3.1
141 |
142 | - fixed contrib-ads loading spinner
143 | - fixed tooltip's z-index when non-linear ad is playing
144 |
145 | ## 0.3.0
146 |
147 | - fixed tech triggering internal adsready event
148 |
149 | ## 0.2.9
150 |
151 | - fixed missing adsready after reset
152 |
153 | ## 0.2.8
154 |
155 | - simplified nopreroll/nopostroll logic
156 | - covers also silent ima errors like skippable on IOS
157 |
158 | ## 0.2.7
159 |
160 | - fixed content tech change
161 | - removed useless code
162 |
163 | ## 0.2.6
164 |
165 | - fixed content tech element reference when content changes
166 | - removed unsafe fullReset, now it fully resets always
167 | - fixed contrib-ads loading-spinner bug
168 |
169 | ## 0.2.5
170 |
171 | - implemented isWaitingForAdBreak() method
172 | - updated contrib-ads dependency
173 |
174 | ## 0.2.4
175 |
176 | - npm version
177 |
178 | ## 0.2.3
179 |
180 | - fixed remainingTime layer index
181 |
182 | ## 0.2.2
183 |
184 | - added showCountdown feature, removed allowVpaid option
185 |
186 | ## 0.2.1
187 |
188 | - reset playToggle when ad skipped when paused
189 |
190 | ## 0.2.0
191 |
192 | - fixed wrong order of tech's play method
193 |
194 | ## 0.1.9
195 |
196 | - prevented ads-loading spinner from content player
197 |
198 | ## 0.1.8
199 |
200 | - added v5 volumebar
201 |
202 | ## 0.1.7
203 |
204 | - npm version
205 |
206 | ## 0.1.6
207 |
208 | - added videojs v5 support
209 |
210 | ## 0.1.0
211 |
212 | - Initial release.
213 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Video ad plugin for video.js
2 |
3 | ## Introduction
4 |
5 | [IMA SDK](https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis) integration
6 | for video.js. Based on customized videojs player, tech and UI tailored for ad playback.
7 |
8 | Note: this is not [official IMA SDK integration](https://github.com/googleads/videojs-ima).
9 |
10 | ## Requirements
11 |
12 | - [IMA SDK](https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis) js binary loaded
13 | - [videojs](https://github.com/videojs/video.js) >= v6.0.1 loaded
14 | - [videojs-contrib-ads](https://github.com/videojs/videojs-contrib-ads) >= v6.2.0 loaded
15 |
16 | ## Installation
17 |
18 | ```
19 | npm install videojs-ima-player
20 | ```
21 |
22 | ## Simple example
23 |
24 | ```html
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
44 |
45 |
46 |
47 |
48 |
49 |
53 |
54 |
55 | ```
56 |
57 | ## Playlist, quality switcher, etc.
58 |
59 | If content player's source is changed, it reinitialize IMA SDK and play ads again. To prevent this behaviour (i.e. switching quality),
60 | you have to set `player.ads.contentSrc="new-source.mp4"` before calling `player.src("new-source.mp4")`.
61 |
62 | ## Methods (bound to player.ima)
63 |
64 | **`updateOptions({options})`** -- sets new IMA options. This options is applied once content player source is changed.
65 |
66 | **`play()`** -- call this method to play ad only when autoPlayAdBreaks is set to false and adBreakReady occurs. Otherwise resumes paused ad.
67 |
68 | **`pause()`** -- pauses current ad.
69 |
70 | ## Events (bound to player.ima)
71 |
72 | [videojs's Player events](https://docs.videojs.com/player#event:beforepluginsetup:$name)
73 |
74 | [additional IMA SDK events](https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type)
75 |
76 | Usage: `player.ima.on(...)`/`player.ima.off(...)`
77 |
78 | ## Settings
79 |
80 | **`adTagUrl`** _(string)_
81 | url of VMAP/VAST/VPAID resource. REQUIRED IF adsResponse IS NOT PROVIDED.
82 |
83 | **`adsResponse`** _(string)_
84 | response in VMAP/VAST/VPAID form. REQUIRED IF adTagUrl IS NOT PROVIDED.
85 |
86 | **`adLabel`** _(string)_
87 | Optional translation for text: "Advertisement". Default: "Advertisement"
88 |
89 | **`adsRenderingSettings`** _(Object)_
90 | [IMA SDK ad rendering settings](https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdsRenderingSettings)
91 |
92 | **`autoPlayAdBreaks`** _(boolean)_
93 | Autoplay ads. Default: true
94 |
95 | **`contribAdsSettings`** _(Object)_
96 | settings for [contrib-ads plugin](http://videojs.github.io/videojs-contrib-ads/integrator/options.html).
97 |
98 | **`debug`** _(boolean)_
99 | contrib-ads debug log. Default: false
100 |
101 | **`disableFlashAds`** _(boolean)_
102 | Disables flash ads. Default: IMA SDK default
103 |
104 | **`disableCustomPlaybackForIOS10Plus`** _(boolean)_
105 | Enables inline playback on iOS 10+. Requires playsinline attribute on video tag. Default: false
106 |
107 | **`forceNonLinearFullSlot`** _(boolean)_
108 | Renders non linear ad as linear fullslot. Default: false
109 |
110 | **`locale`** _(string)_
111 | Sets locale based on [ISO 639-1 (two-letter) or ISO 639-2 (three-letter) code](http://www.loc.gov/standards/iso639-2/php/English_list.php). Default: 'en'
112 |
113 | **`nonLinearWidth`** _(number)_
114 | Sets width of non-linear ads. Default: width of content player
115 |
116 | **`nonLinearHeight`** _(number)_
117 | Sets height of non-linear ads. Default: 1/3 of content player height
118 |
119 | **`numRedirects`** _(number)_
120 | Maximum number of VAST redirects. Default: IMA SDK default
121 |
122 | **`ofLabel`** _(string)_
123 | Optional translation for text "of" (e.g. "1 of 2"). Default: of"
124 |
125 | **`showControlsForJSAds`** (boolean)
126 | Enables controls for VPAID JavaScript ads. Default: true
127 |
128 | **`showCountdown`** _(boolean)_
129 | Enables countdown timer. Default: true
130 |
131 | **`timeout`** _(number)_
132 | contrib-ads hard timeout for loading preroll/postroll ads. Default: 5000
133 |
134 | **`vpaidMode`** _(VpaidMode)_
135 | [google.ima.ImaSdkSettings.VpaidMode](//developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.ImaSdkSettings.VpaidMode). Default: ENABLED
136 |
137 | **`forceSkipTime`** _(number)_
138 | If non-skippable or value is lower than linear's skipOffset, use custom skip button from provided timestamp (seconds). Default: undefined
139 |
140 | **`forceSkipLabel`** _(string)_
141 | Optional translation for text "Skip Ad", Default: "Skip Ad"
142 |
143 | ## Disabled ad autoplay
144 |
145 | Timing of ad playback is handled by IMA SDK. If autoplayAdBreaks is set to false,
146 | this feature is turned off and is up to you when you play the ad
147 | (once adBreakReady is triggered).
148 |
149 | 1. Set `autoPlayAdBreaks` to false
150 | 2. Listen and play on adBreakReady `player.ima.on('adBreakReady', player.ima.play)`
151 |
152 | ## About timeouts
153 |
154 | This integration use hard timeout 5s. If ad is not loaded within given time,
155 | IMA silently skips current ad and resumes content playback. You can adjust this
156 | timeout by `timeout` setting. As IMA SDK supports only one timeout value,
157 | different preroll/postroll timeouts are not supported in this plugin.
158 | Default: `timeout = 5000`, `adsRenderingSettings.loadVideoTimeout = timeout`.
159 |
--------------------------------------------------------------------------------
/config/base.config.mjs:
--------------------------------------------------------------------------------
1 | import json from "@rollup/plugin-json";
2 | import babel from "@rollup/plugin-babel";
3 | import resolve from "@rollup/plugin-node-resolve";
4 | import commonjs from "@rollup/plugin-commonjs";
5 |
6 | export default {
7 | input: "src/ima-plugin.js",
8 | output: {
9 | file: "dist/videojs.ima.js",
10 | format: "umd",
11 | globals: {
12 | "video.js": "videojs",
13 | },
14 | },
15 | watch: {
16 | exclude: ["node_modules/**"],
17 | },
18 | external: ["video.js", "videojs-contrib-ads"],
19 | plugins: [
20 | resolve(),
21 | commonjs(),
22 | json(),
23 | babel({
24 | babelHelpers: "runtime",
25 | exclude: "node_modules/**",
26 | plugins: [
27 | [
28 | "@babel/plugin-transform-runtime",
29 | {
30 | absoluteRuntime: false,
31 | corejs: 3,
32 | helpers: true,
33 | regenerator: true,
34 | useESModules: true,
35 | },
36 | ],
37 | ],
38 | }),
39 | ],
40 | };
41 |
--------------------------------------------------------------------------------
/config/rollup.config.min.mjs:
--------------------------------------------------------------------------------
1 | import config from "./base.config.mjs";
2 | import terser from "@rollup/plugin-terser";
3 | import postcss from "rollup-plugin-postcss";
4 | import autoprefixer from "autoprefixer";
5 | import cssnano from "cssnano";
6 |
7 | config.output.file = "dist/videojs.ima.min.js";
8 | config.plugins.unshift(
9 | postcss({
10 | minimize: true,
11 | extract: true,
12 | plugins: [autoprefixer(), cssnano()],
13 | })
14 | );
15 | config.plugins.push(terser());
16 | export default config;
17 |
--------------------------------------------------------------------------------
/config/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import config from "./base.config.mjs";
2 | import postcss from "rollup-plugin-postcss";
3 | import autoprefixer from "autoprefixer";
4 |
5 | config.plugins.unshift(
6 | postcss({
7 | extract: true,
8 | plugins: [autoprefixer()],
9 | })
10 | );
11 | export default config;
12 |
--------------------------------------------------------------------------------
/examples/advanced/ads.js:
--------------------------------------------------------------------------------
1 | const SAMPLE_AD_TAG =
2 | "http://pubads.g.doubleclick.net/gampad/ads?" +
3 | "sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&" +
4 | "ad_rule=1&impl=s&gdfp_req=1&env=vp&output=xml_vmap1&" +
5 | "unviewed_position_start=1&" +
6 | "cust_params=sample_ar%3Dpremidpostpod%26deployment%3Dgmf-js&cmsid=496&" +
7 | "vid=short_onecue&correlator=";
8 |
9 | class Ads {
10 | options = { debug: true };
11 |
12 | constructor(player) {
13 | this.player = player;
14 | this.player.ima(this.options);
15 | // Set up UI stuff.
16 | this.adTagInput = document.getElementById("tagInput");
17 | const sampleAdTag = document.getElementById("sampleAdTag");
18 | sampleAdTag.addEventListener("click", () => {
19 | this.adTagInput.value = SAMPLE_AD_TAG;
20 | });
21 | this.console = document.getElementById("ima-sample-console");
22 |
23 | const events = [
24 | google.ima.AdEvent.Type.ALL_ADS_COMPLETED,
25 | google.ima.AdEvent.Type.CLICK,
26 | google.ima.AdEvent.Type.COMPLETE,
27 | google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED,
28 | google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED,
29 | google.ima.AdEvent.Type.FIRST_QUARTILE,
30 | google.ima.AdEvent.Type.LOADED,
31 | google.ima.AdEvent.Type.MIDPOINT,
32 | google.ima.AdEvent.Type.PAUSED,
33 | google.ima.AdEvent.Type.STARTED,
34 | google.ima.AdEvent.Type.THIRD_QUARTILE,
35 | ];
36 | for (let index = 0; index < events.length; index++) {
37 | this.player.ima.on(events[index], this.onAdEvent.bind(this));
38 | }
39 |
40 | const applyBtn = document.getElementById("apply-tag");
41 | applyBtn.onclick = this.apply.bind(this);
42 | }
43 |
44 | apply() {
45 | this.player.ima.updateOptions({ adTagUrl: this.adTagInput.value });
46 | var src = this.player.currentSrc();
47 | // source is same so we have to reset before
48 | this.player.reset();
49 | this.player.src(src);
50 | }
51 |
52 | onAdEvent(event) {
53 | this.log(`Ad event: ${event.type}`);
54 | }
55 |
56 | log(message) {
57 | this.console.innerHTML += "
" + message;
58 | }
59 | }
60 |
61 | new Ads(videojs("content_video"));
62 |
--------------------------------------------------------------------------------
/examples/advanced/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
25 |
26 |
27 |
38 |
39 | Video.js ima Plugin
40 |
41 |
42 |
43 |
Video.js IMA Plugin Advanced Demo
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
57 |
58 |
59 |
61 |
62 |
68 |
69 |
70 |
71 | Welcome to IMA HTML5 SDK Demo!
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/examples/autoplay/ads.js:
--------------------------------------------------------------------------------
1 | const video = document.getElementById("content_video");
2 | // ads skippable ads support to IOS
3 | videojs.browser.IS_IOS ? video.setAttribute("playsinline", "") : "";
4 | const isMobile = videojs.browser.IS_IOS || videojs.browser.IS_ANDROID;
5 |
6 | const player = videojs("content_video", {
7 | muted: video.muted || (video.autoplay && isMobile),
8 | });
9 |
10 | player.ima({
11 | debug: true,
12 | adTagUrl:
13 | "http://pubads.g.doubleclick.net/gampad/ads?sz=640x480&" +
14 | "iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&" +
15 | "impl=s&gdfp_req=1&env=vp&output=xml_vmap1&unviewed_position_start=1&" +
16 | "cust_params=sample_ar%3Dpremidpostpod%26deployment%3Dgmf-js&cmsid=496&" +
17 | "vid=short_onecue&correlator=",
18 | });
19 |
--------------------------------------------------------------------------------
/examples/autoplay/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Video.js Test
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/examples/hls/ads.js:
--------------------------------------------------------------------------------
1 | const video = document.getElementById("content_video");
2 | // ads skippable ads support to IOS
3 | videojs.browser.IS_IOS ? video.setAttribute("playsinline", "") : "";
4 | const isMobile = videojs.browser.IS_IOS || videojs.browser.IS_ANDROID;
5 |
6 | const player = videojs("content_video", {
7 | muted: video.muted || (video.autoplay && isMobile),
8 | });
9 |
10 | player.ima({
11 | debug: true,
12 | adTagUrl:
13 | "http://pubads.g.doubleclick.net/gampad/ads?sz=640x480&" +
14 | "iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&" +
15 | "impl=s&gdfp_req=1&env=vp&output=xml_vmap1&unviewed_position_start=1&" +
16 | "cust_params=sample_ar%3Dpremidpostpod%26deployment%3Dgmf-js&cmsid=496&" +
17 | "vid=short_onecue&correlator=",
18 | });
19 |
--------------------------------------------------------------------------------
/examples/hls/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Video.js Test
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/examples/manualplay/ads.js:
--------------------------------------------------------------------------------
1 | const player = videojs("content_video");
2 |
3 | player.ima({
4 | debug: true,
5 | autoPlayAdBreaks: false,
6 | adTagUrl:
7 | "http://pubads.g.doubleclick.net/gampad/ads?sz=640x480&" +
8 | "iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&" +
9 | "impl=s&gdfp_req=1&env=vp&output=xml_vmap1&unviewed_position_start=1&" +
10 | "cust_params=sample_ar%3Dpremidpostpod%26deployment%3Dgmf-js&cmsid=496&" +
11 | "vid=short_onecue&correlator=",
12 | });
13 |
14 | const imaConsole = document.getElementById("ima-sample-console");
15 |
16 | const events = [
17 | google.ima.AdEvent.Type.AD_BREAK_READY,
18 | google.ima.AdEvent.Type.ALL_ADS_COMPLETED,
19 | google.ima.AdEvent.Type.CLICK,
20 | google.ima.AdEvent.Type.COMPLETE,
21 | google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED,
22 | google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED,
23 | google.ima.AdEvent.Type.FIRST_QUARTILE,
24 | google.ima.AdEvent.Type.LOADED,
25 | google.ima.AdEvent.Type.MIDPOINT,
26 | google.ima.AdEvent.Type.PAUSED,
27 | google.ima.AdEvent.Type.STARTED,
28 | google.ima.AdEvent.Type.THIRD_QUARTILE,
29 | ];
30 | for (let index = 0; index < events.length; index++) {
31 | player.ima.on(events[index], onAdEvent);
32 | }
33 |
34 | function onAdEvent(event) {
35 | imaConsole.innerHTML += `
Ad event: ${event.type}`;
36 | }
37 |
38 | player.ima.on(google.ima.AdEvent.Type.AD_BREAK_READY, function () {
39 | player.ima.play();
40 | });
41 |
--------------------------------------------------------------------------------
/examples/manualplay/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Video.js Test
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 |
21 | Welcome to IMA HTML5 SDK Demo!
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/examples/multiple/ads.js:
--------------------------------------------------------------------------------
1 | function pause(id) {
2 | const player = videojs(id);
3 | player.ima.pause();
4 | }
5 |
6 | function initPlayer(id) {
7 | const player = videojs(id);
8 | player.ima({
9 | adTagUrl:
10 | "http://pubads.g.doubleclick.net/gampad/ads?sz=640x480&" +
11 | "iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&" +
12 | "impl=s&gdfp_req=1&env=vp&output=xml_vmap1&unviewed_position_start=1&" +
13 | "cust_params=sample_ar%3Dpremidpostpod%26deployment%3Dgmf-js&cmsid=496&" +
14 | "vid=short_onecue&correlator=",
15 | });
16 | }
17 |
18 | initPlayer("content_video");
19 | initPlayer("content_video1");
20 |
--------------------------------------------------------------------------------
/examples/multiple/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Video.js Test
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
20 |
21 |
22 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/examples/playlist/ads.js:
--------------------------------------------------------------------------------
1 | class Ads {
2 | options = {
3 | adTagUrl:
4 | "http://pubads.g.doubleclick.net/gampad/ads?sz=640x480&" +
5 | "iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&" +
6 | "impl=s&gdfp_req=1&env=vp&output=xml_vmap1&unviewed_position_start=1&" +
7 | "cust_params=sample_ar%3Dpremidpostpod%26deployment%3Dgmf-js&" +
8 | "cmsid=496&vid=short_onecue&correlator=",
9 | };
10 | CONTENTS = [
11 | {
12 | src: "//commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
13 | },
14 | {
15 | src: "//s0.2mdn.net/4253510/google_ddm_animation_480P.mp4",
16 | },
17 | ];
18 |
19 | constructor(player) {
20 | this.player = player;
21 | this.console = document.getElementById("ima-sample-console");
22 |
23 | this.playlistDiv = document.getElementById("ima-sample-playlistDiv");
24 | if (this.playlistDiv) {
25 | this.playlistItems = this.playlistDiv.childNodes;
26 | for (var index in this.playlistItems) {
27 | if (this.playlistItems[index].tagName == "DIV") {
28 | this.playlistItems[index].onclick =
29 | this.onPlaylistItemClick.bind(this);
30 | }
31 | }
32 | }
33 | this.player.ima(this.options);
34 | const events = [
35 | google.ima.AdEvent.Type.ALL_ADS_COMPLETED,
36 | google.ima.AdEvent.Type.CLICK,
37 | google.ima.AdEvent.Type.COMPLETE,
38 | google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED,
39 | google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED,
40 | google.ima.AdEvent.Type.FIRST_QUARTILE,
41 | google.ima.AdEvent.Type.LOADED,
42 | google.ima.AdEvent.Type.MIDPOINT,
43 | google.ima.AdEvent.Type.PAUSED,
44 | google.ima.AdEvent.Type.STARTED,
45 | google.ima.AdEvent.Type.THIRD_QUARTILE,
46 | ];
47 | for (let index = 0; index < events.length; index++) {
48 | this.player.ima.on(events[index], this.onAdEvent.bind(this));
49 | }
50 |
51 | // When the page first loads, don't autoplay. After that, when the user
52 | // clicks a playlist item to switch videos, autoplay.
53 | if (this.playlistItemClicked) {
54 | this.player.play();
55 | }
56 | }
57 |
58 | onAdEvent(event) {
59 | this.console.innerHTML += "
Ad event: " + event.type;
60 | }
61 |
62 | onPlaylistItemClick(event) {
63 | if (!this.player.ads.inAdBreak()) {
64 | this.player.src(this.CONTENTS[event.target.id].src);
65 | }
66 | this.playlistItemClicked = true;
67 | }
68 | }
69 |
70 | new Ads(videojs("content_video"));
71 |
--------------------------------------------------------------------------------
/examples/playlist/img/bbb_preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mysuf/videojs-ima-player/6b7c2140025e36fcae30ae15e887222e5763364d/examples/playlist/img/bbb_preview.jpg
--------------------------------------------------------------------------------
/examples/playlist/img/stock_preview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mysuf/videojs-ima-player/6b7c2140025e36fcae30ae15e887222e5763364d/examples/playlist/img/stock_preview.jpg
--------------------------------------------------------------------------------
/examples/playlist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
23 |
24 |
25 |
36 |
37 | IMA HTML5 SDK Playlist Demo
38 |
39 |
40 |
41 |
IMA HTML5 SDK Playlist Demo
42 |
43 |
44 |
49 |
50 |
51 |
53 |
54 |
60 |
61 |
62 |
63 |
64 |

66 | Video 1
67 |
68 |
69 |

71 | Video 2
72 |
73 |
74 |
75 |
76 | Welcome to IMA HTML5 SDK Demo!
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/examples/reactjs/ads.js:
--------------------------------------------------------------------------------
1 | const SAMPLE_AD_TAG =
2 | "http://pubads.g.doubleclick.net/gampad/ads?" +
3 | "sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&" +
4 | "ad_rule=1&impl=s&gdfp_req=1&env=vp&output=xml_vmap1&" +
5 | "unviewed_position_start=1&" +
6 | "cust_params=sample_ar%3Dpremidpostpod%26deployment%3Dgmf-js&cmsid=496&" +
7 | "vid=short_onecue&correlator=";
8 |
9 | class Ads {
10 | options = { debug: true };
11 |
12 | constructor(player) {
13 | this.player = player;
14 | // Set up UI stuff.
15 | this.adTagInput = document.getElementById("tagInput");
16 | this.sampleAdTag = document.getElementById("sampleAdTag");
17 | this.addSampleFn = this.addSample.bind(this);
18 | this.sampleAdTag.onclick = this.addSampleFn;
19 | this.console = document.getElementById("ima-sample-console");
20 | this.player.ima(this.options);
21 |
22 | const events = [
23 | google.ima.AdEvent.Type.ALL_ADS_COMPLETED,
24 | google.ima.AdEvent.Type.CLICK,
25 | google.ima.AdEvent.Type.COMPLETE,
26 | google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED,
27 | google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED,
28 | google.ima.AdEvent.Type.FIRST_QUARTILE,
29 | google.ima.AdEvent.Type.LOADED,
30 | google.ima.AdEvent.Type.MIDPOINT,
31 | google.ima.AdEvent.Type.PAUSED,
32 | google.ima.AdEvent.Type.STARTED,
33 | google.ima.AdEvent.Type.THIRD_QUARTILE,
34 | ];
35 | for (let index = 0; index < events.length; index++) {
36 | this.player.ima.on(events[index], this.onAdEvent.bind(this));
37 | }
38 |
39 | this.applyBtn = document.getElementById("apply-tag");
40 | this.applyFn = this.apply.bind(this);
41 | this.applyBtn.onclick = this.applyFn;
42 | }
43 |
44 | apply() {
45 | this.player.ima.updateOptions({ adTagUrl: this.adTagInput.value });
46 | const src = this.player.currentSrc();
47 | // source is same so we have to reset before
48 | this.player.reset();
49 | this.player.src(src);
50 | }
51 |
52 | addSample() {
53 | this.adTagInput.value = SAMPLE_AD_TAG;
54 | }
55 |
56 | onAdEvent(event) {
57 | this.log(`Ad event: ${event.type}`);
58 | }
59 |
60 | log(message) {
61 | this.console.innerHTML = this.console.innerHTML + "
" + message;
62 | }
63 |
64 | destroy = function () {
65 | this.sampleAdTag.removeEventListener("click", this.addSampleFn);
66 | this.applyBtn.removeEventListener("click", this.applyFn);
67 | this.player = null;
68 | };
69 | }
70 |
71 | class VideoPlayer extends React.Component {
72 | componentDidMount() {
73 | // instantiate Video.js
74 | this.player = videojs(this.videoNode, this.props);
75 | this.ads = new Ads(this.player);
76 | }
77 |
78 | // destroy player on unmount
79 | componentWillUnmount() {
80 | if (this.player) {
81 | this.ads.destroy();
82 | this.player.dispose();
83 | }
84 | }
85 |
86 | // wrap the player in a div with a `data-vjs-player` attribute
87 | // so videojs won't create additional wrapper in the DOM
88 | // see https://github.com/videojs/video.js/pull/3856
89 | render() {
90 | return React.createElement("video", {
91 | className: "video-js",
92 | ref: (node) => (this.videoNode = node),
93 | });
94 | }
95 | }
96 |
97 | const videojsOptions = {
98 | controls: true,
99 | preload: "auto",
100 | width: 640,
101 | height: 360,
102 | sources: [
103 | {
104 | src: "//commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
105 | type: "video/mp4",
106 | },
107 | ],
108 | };
109 |
110 | ReactDOM.render(
111 | React.createElement(VideoPlayer, videojsOptions),
112 | document.getElementById("ima-sample-videoplayer")
113 | );
114 |
--------------------------------------------------------------------------------
/examples/reactjs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
25 |
26 |
27 |
38 |
39 | Video.js ima Plugin
40 |
41 |
42 |
43 |
Video.js IMA Plugin Advanced Demo
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
55 |
56 |
62 |
63 |
64 |
65 | Welcome to IMA HTML5 SDK Demo!
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/examples/simple/ads.js:
--------------------------------------------------------------------------------
1 | const player = videojs("content_video");
2 |
3 | player.ima({
4 | debug: true,
5 | forceSkipTime: 2,
6 | adTagUrl:
7 | "http://pubads.g.doubleclick.net/gampad/ads?sz=640x480&" +
8 | "iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&" +
9 | "impl=s&gdfp_req=1&env=vp&output=xml_vmap1&unviewed_position_start=1&" +
10 | "cust_params=sample_ar%3Dpremidpostpod%26deployment%3Dgmf-js&cmsid=496&" +
11 | "vid=short_onecue&correlator=",
12 | });
13 |
--------------------------------------------------------------------------------
/examples/simple/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Video.js Test
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/examples/style.css:
--------------------------------------------------------------------------------
1 |
2 | body {
3 | font-family: arial, verdana, sans-serif;
4 | overflow: hidden;
5 | }
6 |
7 | header, footer { text-align:center; width: 100%; }
8 |
9 | header {
10 | height: 40px;
11 | font-size: 40px;
12 | font-weight: bold;
13 | margin-top: 20px;
14 | margin-bottom: 20px;
15 | }
16 |
17 | footer {
18 | margin-top: 20px;
19 | }
20 |
21 | #ima-sample-container {
22 | margin-left: auto;
23 | margin-right: auto;
24 | width: 728px;
25 | }
26 |
27 | #ima-sample-placeholder {
28 | width: 640px;
29 | height: 360px;
30 | }
31 |
32 | .urlLink {
33 | color: blue;
34 | text-decoration: underline;
35 | cursor: pointer;
36 | }
37 |
38 | #ima-sample-videoplayer {
39 | position: relative;
40 | background-color: #000;
41 | border-radius: 5px;
42 | box-shadow: 0px 0px 20px rgba(50, 50, 50, 0.95);
43 | border: 2px #ccc solid;
44 | width: 640px;
45 | height: 360px;
46 | margin-left: auto;
47 | margin-right: auto;
48 | margin-top: 20px;
49 | }
50 |
51 | #content_video {
52 | overflow: hidden;
53 | }
54 |
55 | #ima-sample-content-wrapper {
56 | position:relative;
57 | top: 0px;
58 | left: 0px
59 | }
60 |
61 | #ima-sample-playpause, #ima-sample-replay {
62 | position: absolute;
63 | left: 20px;
64 | bottom: 20px;
65 | height: 40px;
66 | width: 100px;
67 | border-style: none;
68 | font-weight: bold;
69 | font-size: 25px;
70 | opacity: 0.5;
71 | background-color: #fff;
72 | border-radius: 5px;
73 | border: 1px transparent solid;
74 | color: #000;
75 | cursor: pointer;
76 | line-height: 0;
77 | }
78 |
79 | #ima-sample-replay {
80 | display: none;
81 | }
82 |
83 | #ima-sample-playpause:hover, #ima-sample-replay:hover {
84 | border: 1px #f00 solid;
85 | color: #f00;
86 | }
87 |
88 | #ima-sample-fullscreen {
89 | position: absolute;
90 | bottom: 20px;
91 | right: 20px;
92 | height: 40px;
93 | width: 100px;
94 | border-style: none;
95 | font-weight: bold;
96 | font-size: 25px;
97 | opacity: 0.5;
98 | background-color: #fff;
99 | border-radius: 5px;
100 | border: 1px transparent solid;
101 | color: #000;
102 | cursor: pointer;
103 | line-height: 0;
104 | }
105 |
106 | #ima-sample-fullscreen:hover {
107 | border: 1px #f00 solid;
108 | color: #f00;
109 | }
110 |
111 | #ima-sample-content, #ima-sample-adcontainer {
112 | position: absolute;
113 | top: 0px;
114 | left: 0px;
115 | width: 640px;
116 | height: 360px;
117 | }
118 |
119 | #ima-sample-playlistDiv {
120 | height: 122px;
121 | width: 224px;
122 | margin-left: auto;
123 | margin-right: auto;
124 | margin-top: 10px;
125 | margin-bottom: 10px;
126 | text-align: center;
127 | }
128 |
129 | .ima-sample-playlistItem {
130 | height: 122px;
131 | width: 102px;
132 | float: left;
133 | margin-right: 10px;
134 | cursor: pointer;
135 | }
136 |
137 | .ima-sample-playlistImg {
138 | border: 1px solid;
139 | }
140 |
141 | #ima-sample-console {
142 | font-family: courier, monospace;
143 | font-size: 12px;
144 | margin-top: 20px;
145 | height: 200px;
146 | width: 630px;
147 | padding: 5px;
148 | border: 1px #ccc solid;
149 | overflow-y: scroll;
150 | margin-left: auto;
151 | margin-right: auto;
152 | }
153 |
154 | #ima-sample-companionDiv {
155 | width: 728px;
156 | height: 90px;
157 | margin-top: 20px;
158 | }
159 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | IMA Plugin for Video.js
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "videojs-ima-player",
3 | "version": "0.6.6",
4 | "license": "MIT",
5 | "main": "./dist/videojs.ima.js",
6 | "author": {
7 | "name": "Petr Schuchmann"
8 | },
9 | "engines": {
10 | "node": ">=18.17.1"
11 | },
12 | "scripts": {
13 | "lint": "eslint \"src/*.js\"",
14 | "rollup": "npm-run-all rollup:*",
15 | "rollup:max": "rollup -c config/rollup.config.mjs",
16 | "rollup:min": "rollup -c config/rollup.config.min.mjs",
17 | "test": "npm-run-all test:*",
18 | "test:vjs6latest": "npm install video.js@6.10.0 --no-save && npm run rollup:min && npm-run-all -p -r testServer webdriver",
19 | "test:vjs7first": "npm install video.js@7.0.0 --no-save && npm run rollup:min && npm-run-all -p -r testServer webdriver",
20 | "test:vjs7latest": "npm install video.js@\"<8.0.0\" --no-save && npm run rollup:min && npm-run-all -p -r testServer webdriver",
21 | "testServer": "http-server --cors -p 8000 --silent",
22 | "webdriver": "mocha test/webdriver/*.mjs --no-timeouts"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "https://github.com/mysuf/videojs-ima-player"
27 | },
28 | "files": [
29 | "CHANGELOG.md",
30 | "LICENSE",
31 | "README.md",
32 | "dist/",
33 | "examples/"
34 | ],
35 | "dependencies": {
36 | "video.js": "^6.10.0 || ^7",
37 | "videojs-contrib-ads": "^6.7.0"
38 | },
39 | "devDependencies": {
40 | "@babel/cli": "^7.24.8",
41 | "@babel/core": "^7.25.2",
42 | "@babel/eslint-parser": "^7.25.1",
43 | "@babel/plugin-transform-arrow-functions": "^7.24.7",
44 | "@babel/plugin-transform-class-properties": "^7.25.4",
45 | "@babel/plugin-transform-modules-commonjs": "^7.24.8",
46 | "@babel/plugin-transform-runtime": "^7.25.4",
47 | "@babel/preset-env": "^7.25.4",
48 | "@babel/runtime": "^7.25.4",
49 | "@babel/runtime-corejs3": "^7.25.0",
50 | "@rollup/plugin-babel": "^6.0.4",
51 | "@rollup/plugin-commonjs": "^26.0.1",
52 | "@rollup/plugin-json": "^6.1.0",
53 | "@rollup/plugin-node-resolve": "^15.2.3",
54 | "@rollup/plugin-terser": "^0.4.4",
55 | "autoprefixer": "^10.4.20",
56 | "chromedriver": "^128.0.0",
57 | "core-js": "^3.38.1",
58 | "cssnano": "^7.0.5",
59 | "eslint": "^8.57.0",
60 | "eslint-config-google": "^0.14.0",
61 | "eslint-plugin-jsdoc": "^46.10.1",
62 | "geckodriver": "^4.4.3",
63 | "http-server": "^14.1.1",
64 | "mocha": "^10.7.3",
65 | "npm-run-all": "^4.1.5",
66 | "regenerator-runtime": "^0.13.9",
67 | "rollup": "^4.21.0",
68 | "rollup-plugin-includepaths": "^0.2.4",
69 | "rollup-plugin-postcss": "^4.0.2",
70 | "selenium-webdriver": "^4.23.0"
71 | },
72 | "keywords": [
73 | "videojs",
74 | "videojs-plugin",
75 | "videojs-ads"
76 | ]
77 | }
--------------------------------------------------------------------------------
/src/css/videojs.ima.css:
--------------------------------------------------------------------------------
1 | /*
2 | as default videojs styles and skins try to override
3 | ima player styles, important is used alot to get highest prio
4 | no matter how accurate path is
5 | */
6 | .vjs-controls-disabled .vjs-ima.vjs-controls-enabled .vjs-control-bar,
7 | .vjs-using-native-controls .vjs-ima.vjs-controls-enabled .vjs-control-bar,
8 | .vjs-error .vjs-ima.vjs-controls-enabled .vjs-control-bar {
9 | display: flex !important;
10 | }
11 | .vjs-ad-loading .vjs-ima .vjs-loading-spinner {
12 | display: none;
13 | }
14 | .vjs-ima {
15 | position: absolute !important;
16 | top: 0 !important;
17 | left: 0 !important;
18 | height: 100% !important;
19 | width: 100% !important;
20 | overflow: hidden !important;
21 | font-size: 10px !important;
22 | line-height: 1 !important;
23 | background-color: transparent;
24 | }
25 | .vjs-ima .ima-ad-container {
26 | z-index: 1111 !important;
27 | }
28 | .vjs-ima .vjs-control-bar {
29 | height: 3em !important;
30 | padding: 0 !important;
31 | padding-bottom: 0.25em !important;
32 | }
33 | .vjs-ima .vjs-control-bar,
34 | .video-js.non-linear-ad .vjs-control-bar,
35 | .vjs-ima.vjs-waiting .vjs-loading-spinner {
36 | z-index: 1112 !important;
37 | }
38 |
39 | .non-linear-ad .vjs-ima .ima-ad-container > div:first-child {
40 | bottom: 0;
41 | }
42 | .vjs-user-active.non-linear-ad .vjs-ima,
43 | .vjs-paused.non-linear-ad .vjs-ima,
44 | .vjs-ended.non-linear-ad .vjs-ima,
45 | .video-js.non-linear-ad .vjs-ima.vjs-user-active {
46 | height: auto !important;
47 | bottom: 3em !important;
48 | }
49 |
50 | .vjs-ima .vjs-play-progress:before {
51 | display: none !important;
52 | }
53 |
54 | .vjs-ima .vjs-remaining-time {
55 | position: static !important;
56 | }
57 |
58 | .vjs-ima .vjs-remaining-time:not(.vjs-hidden) {
59 | display: block !important;
60 | }
61 |
62 | .vjs-ima .vjs-remaining-time-display {
63 | font-size: 1.05em;
64 | position: relative !important;
65 | }
66 |
67 | .vjs-ima.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar {
68 | opacity: 1 !important;
69 | pointer-events: none !important;
70 | background-color: transparent !important;
71 | }
72 |
73 |
74 | .vjs-ima.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-remaining-time:before {
75 | content: '' !important;;
76 | position: absolute !important;;
77 | z-index: -1 !important;;
78 | top: 0 !important;
79 | left: 0 !important;;
80 | right: 0 !important;;
81 | bottom: 0 !important;;
82 | background-image: radial-gradient(350px 3em at bottom left, rgba(43,51,63,.7), transparent) !important;
83 | }
84 |
85 | /* move any pseudo backgrounds etc to foreground */
86 | .vjs-ima .vjs-control-bar:before,
87 | .vjs-ima .vjs-control-bar:after {
88 | pointer-events: none !important;
89 | }
90 | .vjs-ima.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar:before,
91 | .vjs-ima.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar:after {
92 | display: none !important;
93 | }
94 |
95 | .vjs-ima.vjs-has-started.vjs-playing .vjs-control-bar > * {
96 | transition: all 0.3s !important;
97 | opacity: 1 !important;
98 | visibility: visible !important;
99 | }
100 |
101 | .vjs-ima.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar > *:not(.vjs-remaining-time):not(.vjs-progress-control) {
102 | width: 0 !important;
103 | visibility: hidden !important;
104 | opacity: 0 !important;
105 | }
106 |
107 | .vjs-ima.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar > .vjs-remaining-time {
108 | text-shadow: 0 0 10px #000;
109 | }
110 |
111 | .vjs-ima .vjs-custom-control-spacer {
112 | display: block !important;
113 | flex: auto !important;
114 | }
115 |
116 | .vjs-ima.vjs-has-started .vjs-control-bar > .vjs-progress-control {
117 | position: absolute !important;
118 | pointer-events: none !important;
119 | left: 0 !important;
120 | bottom: 0 !important;
121 | width: 100% !important;
122 | display: block !important;
123 | height: 0.25em !important;
124 | top: unset !important;
125 | }
126 |
127 | .vjs-ima .vjs-progress-control .vjs-progress-holder {
128 | margin: 0 !important;
129 | }
130 |
131 | .vjs-ima .vjs-ima-skip-button {
132 | position: absolute;
133 | right: 0;
134 | bottom: 37px;
135 | z-index: 1112;
136 | background: black;
137 | font-size: 20px;
138 | border: 1px solid white;
139 | border-right: 0;
140 | padding: 9px 14px;
141 | }
142 |
--------------------------------------------------------------------------------
/src/ima-player.js:
--------------------------------------------------------------------------------
1 | import videojs from "video.js";
2 | import "./ima-time-display.js";
3 | import "./ima-tech.js";
4 | import "./ima-skip-button.js";
5 |
6 | const Player = videojs.getComponent("Player");
7 |
8 | // Player is subclass of Component so is usable as part of parent player
9 | // plus is fully customizable and independent from content player
10 | class ImaPlayer extends Player {
11 | constructor(contentPlayer, options) {
12 | // serve tag placeholder to player
13 |
14 | const adPlayerContainer = document.createElement("div");
15 | adPlayerContainer.id = contentPlayer.id_ + "_ima";
16 | adPlayerContainer.className = "vjs-ima video-js";
17 |
18 | options.src = options.adTagUrl || options.adsResponse || "placeholder";
19 | options.type = "video/ima";
20 |
21 | // sets basic player
22 | // passes src placeholder to tech
23 | // sets customized remaining time component
24 | super(adPlayerContainer, {
25 | controls: false,
26 | sources: [options],
27 | techOrder: ["ima"],
28 | controlBar: {
29 | imaRemainingTimeDisplay: {
30 | adLabel: options.adLabel || "Advertisement",
31 | ofLabel: options.ofLabel || "of",
32 | },
33 | children: [
34 | "playToggle",
35 | "volumePanel",
36 | "imaRemainingTimeDisplay",
37 | "progressControl",
38 | "customControlSpacer",
39 | "fullscreenToggle",
40 | ],
41 | },
42 | skipButton: {
43 | skipTime: options.forceSkipTime,
44 | skipLabel: options.forceSkipLabel,
45 | },
46 | children: [
47 | "mediaLoader",
48 | "loadingSpinner",
49 | "controlBar",
50 | "skipButton",
51 | "errorDisplay",
52 | ],
53 | });
54 |
55 | this.resizeType = contentPlayer.resizeManager
56 | ? "playerresize"
57 | : ["resize", "fullscreenchange"];
58 |
59 | this.hide();
60 |
61 | // remove it from exposed players in case that somebody
62 | // would manipulate with them globally
63 | Player.players[this.id_] = null;
64 |
65 | this.imaOptions = options;
66 |
67 | // through events we have these values up to date
68 | // and exposed for component imaRemainingTimeDisplay
69 | this.contentVideoElement = null;
70 | this.adPosition = 0;
71 | this.totalAds = 0;
72 | this.adsReadyTriggered = false;
73 | this.noPreroll = false;
74 | this.noPostroll = false;
75 | this.contentHasStarted_ = false;
76 |
77 | // we wont toggle content player controls if controls disabled
78 | this.contentControlsDisabled = !contentPlayer.controls();
79 | this.contentPlayer = contentPlayer;
80 | // setting src directly on video element bypasses player's techs
81 | // and creates issues when it should switch tech, f.e. HLS <-> HTML5
82 | // this watch changing of src and redirects to player api (IOS)
83 | this.srcObserver = new MutationObserver(function (mutations) {
84 | mutations.forEach(function (mutation) {
85 | if (
86 | mutation.target[mutation.attributeName] !==
87 | mutation.oldValue
88 | ) {
89 | contentPlayer.src(mutation.target[mutation.attributeName]);
90 | }
91 | });
92 | });
93 |
94 | this.isMobile = videojs.browser.IS_IOS || videojs.browser.IS_ANDROID;
95 | if (this.isMobile) this.addClass("vjs-ima-mobile");
96 |
97 | this.setRemainingTimeVisibility();
98 | this.trackContentEvents();
99 |
100 | // wait a tick to get content info
101 | contentPlayer.ready(() => {
102 | this.contentVideoElement = this.getContentTechElement();
103 |
104 | if (!this.contentVideoElement) {
105 | return;
106 | }
107 |
108 | this.tech_.handleLateInit_({
109 | imaPlayer: this,
110 | mediaElement: this.contentVideoElement,
111 | width: contentPlayer.currentWidth(),
112 | height: contentPlayer.currentHeight(),
113 | volume: contentPlayer.volume(),
114 | fullscreen: contentPlayer.isFullscreen(),
115 | autoplay: contentPlayer.autoplay(),
116 | muted: contentPlayer.muted(),
117 | });
118 | this.handleContentResize_();
119 | });
120 | }
121 |
122 | // OVERRIDES default method
123 | // we want aditional events on loadTech
124 | loadTech_(techName, source) {
125 | super.loadTech_.call(this, techName, source);
126 | this.trackImaEvents();
127 | }
128 |
129 | // OVERRIDES default method
130 | // calls api through contentPlayer
131 | requestFullscreen() {
132 | if (!this.contentPlayer.isFullscreen()) {
133 | this.contentPlayer.requestFullscreen();
134 | }
135 | }
136 |
137 | // OVERRIDES default method
138 | // calls api through contentPlayer
139 | exitFullscreen() {
140 | if (this.contentPlayer.isFullscreen()) {
141 | this.contentPlayer.exitFullscreen();
142 | }
143 | }
144 |
145 | // OVERRIDES default method
146 | // we wont reset waiting on timeupdate
147 | // because tracker must run also during ads
148 | handleTechWaiting_() {
149 | this.addClass("vjs-waiting");
150 | this.trigger("waiting");
151 | }
152 |
153 | // OVERRIDES default method
154 | // there are aditional jobs that needs to be done
155 | reset() {
156 | this.setContentPlayerToDefault();
157 | this.noPreroll = false;
158 | this.noPostroll = false;
159 | super.reset.call(this);
160 | this.handleTechAdsReady_();
161 | }
162 |
163 | /* THESE METHODS ARE PART OF TECH INITIALIZATION */
164 |
165 | trackContentEvents() {
166 | this.on(this.contentPlayer, "seek", this.handleContentSeek_);
167 | this.on(this.contentPlayer, "seeked", this.handleContentSeeked_);
168 | this.on(
169 | this.contentPlayer,
170 | "durationchange",
171 | this.handleContentDurationChange_
172 | );
173 | this.on(
174 | this.contentPlayer,
175 | "timeupdate",
176 | this.handleContentTimeUpdate_
177 | );
178 | this.on(this.contentPlayer, this.resizeType, this.handleContentResize_);
179 | this.on(
180 | this.contentPlayer,
181 | "contentupdate",
182 | this.handleContentChanged_
183 | );
184 | this.on(
185 | this.contentPlayer,
186 | "readyforpreroll",
187 | this.handleContentReadyForPreroll_
188 | );
189 | this.on(
190 | this.contentPlayer,
191 | "readyforpostroll",
192 | this.handleContentReadyForPostroll_
193 | );
194 | }
195 |
196 | trackImaEvents() {
197 | // these events are removed together with tech
198 | this.on(this.tech_, "adsready", this.handleTechAdsReady_);
199 | this.on(this.tech_, "adchange", this.handleTechAdChange_);
200 | this.on(this.tech_, "linearadstarted", this.handleTechLinearAdStarted_);
201 | this.on(this.tech_, "linearadended", this.handleTechLinearAdEnded_);
202 | this.on(
203 | this.tech_,
204 | "nonlinearadstarted",
205 | this.handleTechNonLinearAdStarted_
206 | );
207 | this.on(
208 | this.tech_,
209 | "nonlinearadended",
210 | this.handleTechNonLinearAdEnded_
211 | );
212 | this.on(this.tech_, "adserror", this.handleTechAdsError_);
213 | }
214 |
215 | setContentPlayerToDefault() {
216 | this.handleTechLinearAdEnded_();
217 | this.handleTechNonLinearAdEnded_();
218 | }
219 |
220 | getContentTechElement() {
221 | if (!this.contentPlayer.tech_ || !this.contentPlayer.tech_.el_) {
222 | return;
223 | }
224 | if (this.contentPlayer.techName_ !== "Html5") {
225 | ["canPlayType", "play", "pause"].forEach((method) => {
226 | if (!this.contentPlayer.tech_.el_[method]) {
227 | this.contentPlayer.tech_.el_[method] = () => false;
228 | }
229 | });
230 | }
231 | return this.contentPlayer.tech_.el_;
232 | }
233 |
234 | setRemainingTimeVisibility() {
235 | if (this.imaOptions.showCountdown === false) {
236 | this.controlBar.imaRemainingTimeDisplay.hide();
237 | return;
238 | }
239 | this.controlBar.imaRemainingTimeDisplay.show();
240 | }
241 |
242 | /* IMA PLAYER METHODS USABLE FROM GLOBAL SPACE (PUBLIC) */
243 |
244 | updateOptions(options) {
245 | if (this.imaOptions && options) {
246 | Object.assign(this.imaOptions, options);
247 | }
248 |
249 | // force next call player.src to reset contrib-ads
250 | // even if source is the same
251 | this.contentPlayer.ads.contentSrc = "";
252 | }
253 |
254 | /* THESE METHODS CONTROLS CONTENT PLAYER */
255 |
256 | resumeContent() {
257 | if (this.contentHasStarted_ && !this.contentEnded) {
258 | this.contentPlayer.play();
259 | }
260 | }
261 |
262 | pauseContent() {
263 | if (this.contentHasStarted_ && !this.contentEnded) {
264 | this.contentPlayer.pause();
265 | }
266 | }
267 |
268 | setContentControls(bool) {
269 | if (!this.contentControlsDisabled) {
270 | this.contentPlayer.controls(bool);
271 | }
272 | }
273 |
274 | skipLinearAdMode() {
275 | if (this.contentPlayer.ads.isWaitingForAdBreak()) {
276 | this.contentPlayer.ads.skipLinearAdMode();
277 | }
278 | }
279 |
280 | /* THESE METHODS HANDLES CONTENT PLAYER */
281 |
282 | handleContentReadyForPreroll_() {
283 | this.contentHasStarted_ = true;
284 | if (this.noPreroll) {
285 | this.skipLinearAdMode();
286 | }
287 | this.techCall_("preroll");
288 | this.noPreroll = true;
289 | }
290 |
291 | handleContentReadyForPostroll_() {
292 | // triggers only once per source
293 | if (this.noPostroll) {
294 | this.skipLinearAdMode();
295 | }
296 | if (!this.contentEnded) {
297 | this.contentEnded = true;
298 | this.techCall_("postroll");
299 | }
300 | this.noPostroll = true;
301 | }
302 |
303 | handleContentChanged_() {
304 | this.setContentPlayerToDefault();
305 | this.imaOptions.contentMediaElement = this.getContentTechElement();
306 | if (!this.imaOptions.contentMediaElement) {
307 | return;
308 | }
309 | this.contentEnded = false;
310 | this.noPreroll = false;
311 | this.noPostroll = false;
312 | this.adsReadyTriggered = false;
313 | this.src(this.imaOptions);
314 | this.setRemainingTimeVisibility();
315 | }
316 |
317 | handleContentTimeUpdate_() {
318 | this.ready(() => {
319 | this.tech_.contentTracker.previousTime =
320 | this.tech_.contentTracker.currentTime;
321 | this.tech_.contentTracker.currentTime =
322 | this.contentPlayer.currentTime();
323 | });
324 | }
325 |
326 | handleContentResize_() {
327 | this.isFullscreen(this.contentPlayer.isFullscreen());
328 | this.techCall_("resize", {
329 | width: this.contentPlayer.currentWidth(),
330 | height: this.contentPlayer.currentHeight(),
331 | fullscreen: this.isFullscreen(),
332 | });
333 | }
334 |
335 | handleContentDurationChange_() {
336 | this.ready(() => {
337 | this.tech_.contentTracker.duration = this.contentPlayer.duration();
338 | });
339 | }
340 |
341 | handleContentSeek_() {
342 | this.ready(() => {
343 | this.tech_.contentTracker.seeking = true;
344 | });
345 | }
346 |
347 | handleContentSeeked_() {
348 | this.ready(() => {
349 | this.tech_.contentTracker.seeking = false;
350 | });
351 | }
352 |
353 | /* THESE METHODS HANDLES IMA TECH */
354 |
355 | handleTechAdsReady_(e, cuePoints) {
356 | this.noPreroll = !cuePoints;
357 | this.noPostroll = !cuePoints || !cuePoints.includes(-1);
358 | if (!this.adsReadyTriggered) {
359 | this.adsReadyTriggered = true;
360 | this.contentPlayer.trigger("adsready");
361 | }
362 | }
363 |
364 | handleTechAdChange_(e, adPodInfo) {
365 | this.adPosition = adPodInfo.adPosition;
366 | this.totalAds = adPodInfo.totalAds;
367 | }
368 |
369 | handleTechLinearAdStarted_(e, isControlsAllowed) {
370 | if (this.contentPlayer.ads.inAdBreak()) {
371 | return;
372 | }
373 |
374 | this.volume(this.contentPlayer.volume());
375 | this.muted(this.contentPlayer.muted());
376 | this.contentPlayer.ads.startLinearAdMode();
377 | this.contentVideoElement &&
378 | this.srcObserver.observe(this.contentVideoElement, {
379 | attributes: true,
380 | attributeFilter: ["src"],
381 | attributeOldValue: true,
382 | });
383 | this.contentPlayer.trigger("ads-ad-started");
384 | this.setContentControls(false);
385 | this.controls(isControlsAllowed);
386 | this.pauseContent();
387 | this.show();
388 | }
389 |
390 | handleTechLinearAdEnded_() {
391 | this.srcObserver.disconnect();
392 | if (this.contentPlayer.ads.inAdBreak()) {
393 | this.contentPlayer.volume(this.volume());
394 | this.contentPlayer.muted(this.muted());
395 | this.contentPlayer.ads.endLinearAdMode();
396 | } else {
397 | // covers silent errors like skippable on IOS
398 | this.skipLinearAdMode();
399 | }
400 |
401 | this.controls(false);
402 | this.setContentControls(true);
403 | this.hide();
404 | this.resumeContent();
405 | }
406 |
407 | handleTechNonLinearAdStarted_() {
408 | if (!this.contentPlayer.ads.inAdBreak()) {
409 | this.skipLinearAdMode();
410 | }
411 | this.controls(false);
412 | this.contentPlayer.addClass("non-linear-ad");
413 | this.show();
414 | }
415 |
416 | handleTechNonLinearAdEnded_() {
417 | this.contentPlayer.removeClass("non-linear-ad");
418 | this.hide();
419 | }
420 |
421 | handleTechAdsError_() {
422 | this.hide();
423 | this.removeClass("waiting");
424 | this.reset();
425 | }
426 | }
427 |
428 | // registers player as normal component
429 | videojs.registerComponent("imaPlayer", ImaPlayer);
430 |
--------------------------------------------------------------------------------
/src/ima-plugin.js:
--------------------------------------------------------------------------------
1 | import "./css/videojs.ima.css";
2 |
3 | import videojs from "video.js";
4 | import "videojs-contrib-ads";
5 | import "./ima-player.js";
6 |
7 | // basic plugin is enough for this purpose
8 | videojs.registerPlugin("ima", function (options) {
9 | // inits contrib-ads asap if not initialized yet
10 | if (!this.ads) {
11 | console.error(
12 | "ima-player error: contrib-ads must be registered on player."
13 | );
14 | return;
15 | }
16 |
17 | if (typeof this.ads === "function") {
18 | this.ads(
19 | Object.assign(
20 | {
21 | debug: options.debug || false,
22 | timeout: options.timeout || 5000,
23 | },
24 | options.contribAdsSettings || {}
25 | )
26 | );
27 | }
28 |
29 | this.ima = this.addChild("imaPlayer", options);
30 | });
31 |
--------------------------------------------------------------------------------
/src/ima-skip-button.js:
--------------------------------------------------------------------------------
1 | import videojs from "video.js";
2 |
3 | const Component = videojs.getComponent("Component");
4 |
5 | class SkipButton extends Component {
6 | constructor(player, options) {
7 | super(player, options);
8 | this.active = false;
9 | this.resetState();
10 |
11 | if (
12 | !google?.ima ||
13 | typeof options.skipTime !== "number" ||
14 | isNaN(options.skipTime)
15 | ) {
16 | return;
17 | }
18 |
19 | this.on(player, google.ima.AdEvent.Type.COMPLETE, () => {
20 | this.resetState();
21 | });
22 |
23 | this.on(player, "timeupdate", () => {
24 | if (this.isForcedSkipEnabled()) {
25 | this.active = true;
26 | this.removeClass("vjs-hidden");
27 | }
28 | });
29 |
30 | this.on("click", () => {
31 | this.resetState();
32 | player.techCall_("forceSkip");
33 | });
34 | }
35 |
36 | isForcedSkipEnabled() {
37 | const player = this.player();
38 | const currentAdSkipOffset =
39 | player.tech_?.currentAd?.getSkipTimeOffset() ?? -1;
40 | // fix videojs or browser bug where currentTime() on init shows greater value than duration itself
41 | const currentTime = player.currentTime();
42 | const duration = player.duration();
43 | return (
44 | !this.active &&
45 | (currentAdSkipOffset < 0 ||
46 | currentAdSkipOffset > this.options_.skipTime) &&
47 | currentTime &&
48 | duration &&
49 | currentTime < duration &&
50 | duration - this.options_.skipTime > 5 &&
51 | currentTime > this.options_.skipTime
52 | );
53 | }
54 |
55 | resetState() {
56 | this.active = false;
57 | this.addClass("vjs-hidden");
58 | }
59 |
60 | createEl() {
61 | return super.createEl.call(
62 | this,
63 | "button",
64 | {
65 | className: "vjs-ima-skip-button",
66 | textContent: this.options_.skipLabel || "Skip Ad",
67 | },
68 | {
69 | "aria-live": "off",
70 | "aria-atomic": "true",
71 | }
72 | );
73 | }
74 | }
75 |
76 | videojs.registerComponent("skipButton", SkipButton);
77 |
--------------------------------------------------------------------------------
/src/ima-tech.js:
--------------------------------------------------------------------------------
1 | import videojs from "video.js";
2 | import { version } from "../package.json";
3 |
4 | const Tech = videojs.getTech("Tech");
5 |
6 | class Ima extends Tech {
7 | constructor(options, ready) {
8 | super(options, ready);
9 |
10 | this.contentTracker = {
11 | previousTime: 0,
12 | currentTime: 0,
13 | duration: 0,
14 | seeking: false,
15 | };
16 |
17 | this.currentAd = null;
18 | this.cuePoints = [];
19 | this.source = options.source;
20 | this.adDisplayContainer = null;
21 | this.adsLoader = null;
22 | this.adsManager = null;
23 | this.width = 0;
24 | this.heght = 0;
25 | this.screenMode = "";
26 | this.volume_ = 1;
27 | this.muted_ = false;
28 | this.currentTime_ = 0;
29 |
30 | // initialized later via handleLateInit_ method
31 | // called by ImaPlayer
32 | }
33 |
34 | /* DEFAULT IMA SOURCE OPTIONS */
35 |
36 | mergeWithDefaults(options) {
37 | var gis = google.ima.settings;
38 | return Object.assign(
39 | {
40 | showControlsForJSAds: true,
41 | locale: gis.getLocale(),
42 | disableFlashAds: gis.getDisableFlashAds(),
43 | disableCustomPlaybackForIOS10Plus: videojs.browser.IS_IOS,
44 | numRedirects: gis.getNumRedirects(),
45 | autoPlayAdBreaks: true,
46 | vpaidMode: google.ima.ImaSdkSettings.VpaidMode.ENABLED,
47 | adTagUrl: "",
48 | adsResponse: "",
49 | forceNonLinearFullSlot: false,
50 | nonLinearWidth: 0,
51 | nonLinearHeight: 0,
52 | adWillAutoPlay: false,
53 | adWillPlayMuted: false,
54 | showCountdown: true,
55 | adsRenderingSettings: {
56 | loadVideoTimeout: options.timeout || 5000,
57 | },
58 | },
59 | options
60 | );
61 | }
62 |
63 | /* THESE ARE Tech's OVERRIDEN METHODS */
64 |
65 | createEl() {
66 | var divWrapper = document.createElement("div");
67 | divWrapper.className = "vjs-tech ima-ad-container";
68 | divWrapper.id = this.options_.playerId + "-ad-container";
69 |
70 | return divWrapper;
71 | }
72 |
73 | controls() {
74 | return false;
75 | }
76 |
77 | poster() {
78 | return null;
79 | }
80 |
81 | setPoster() {}
82 |
83 | src(source) {
84 | this.setSource(source);
85 | return this.source;
86 | }
87 |
88 | currentSrc() {
89 | return this.source.adTagUrl || this.source.adsResponse || "";
90 | }
91 |
92 | setSource(source, init) {
93 | if (!source || typeof source !== "object") {
94 | return;
95 | }
96 |
97 | this.source = this.mergeWithDefaults(source);
98 | if (!init) this.reset();
99 | this.trigger("loadstart"); // resets player classes
100 |
101 | if (!this.source.adTagUrl && !this.source.adsResponse) {
102 | // if no ads are provided we left tech reseted
103 | // and let content know that no ads will be played
104 | if (init) this.triggerReady();
105 | this.trigger("adsready");
106 | return;
107 | }
108 |
109 | this.isReady_ = false;
110 | this.trigger("waiting");
111 | this.initAdContainer();
112 | this.requestAds();
113 | }
114 |
115 | autoplay() {
116 | return this.source && this.source.autoPlayAdBreaks;
117 | }
118 |
119 | setAutoplay() {}
120 |
121 | loop() {
122 | return false;
123 | }
124 |
125 | setLoop() {}
126 |
127 | play() {
128 | // state order dispatching
129 | if (!this.isReady_) {
130 | console.warn("Ads warning: ads not ready to play yet.");
131 | return;
132 | }
133 |
134 | if (!this.adsManager || this.ended()) {
135 | console.warn("Ads warning: No ads.");
136 | return;
137 | }
138 |
139 | if (!this.contentHasStarted_) {
140 | console.warn("Ads warning: content must be playing.");
141 | return;
142 | }
143 |
144 | if (this.isLinearAd() && this.paused()) {
145 | this.adsManager.resume();
146 | return;
147 | }
148 |
149 | if (!this.hasStarted_ || !this.autoplay()) {
150 | this.start();
151 | return;
152 | }
153 | }
154 |
155 | pause() {
156 | if (this.isLinearAd() && !this.paused()) {
157 | this.adsManager.pause();
158 | }
159 | }
160 |
161 | paused() {
162 | return !!this.paused_;
163 | }
164 |
165 | currentTime() {
166 | return this.currentTime_;
167 | }
168 |
169 | setCurrentTime(seconds) {
170 | this.currentTime_ = seconds ?? 0;
171 | }
172 |
173 | seeking() {
174 | return false;
175 | }
176 |
177 | seekable() {
178 | return videojs.createTimeRange();
179 | }
180 |
181 | playbackRate() {
182 | return 1.0;
183 | }
184 |
185 | duration() {
186 | return this.currentAd && this.currentAd.getDuration() > 0
187 | ? this.currentAd.getDuration()
188 | : 0;
189 | }
190 |
191 | ended() {
192 | return !!this.ended_;
193 | }
194 |
195 | volume() {
196 | return this.volume_;
197 | }
198 |
199 | // throttle volume change (to reduce event emits)
200 | setManagerVolume(vol) {
201 | clearTimeout(this.volTimeout);
202 | this.volTimeout = setTimeout(
203 | () => this.adsManager && this.adsManager.setVolume(vol),
204 | 250
205 | );
206 | }
207 |
208 | setVolume(vol) {
209 | if (vol === this.volume_) return;
210 |
211 | this.volume_ = vol;
212 | this.muted_ = !vol;
213 | this.trigger("volumechange");
214 | this.setManagerVolume(vol);
215 | }
216 |
217 | muted() {
218 | return this.muted_;
219 | }
220 |
221 | setMuted(mute) {
222 | if (mute == this.muted_) return;
223 |
224 | this.muted_ = mute;
225 | this.trigger("volumechange");
226 | this.setManagerVolume(!mute ? this.volume_ : 0);
227 | }
228 |
229 | buffered() {
230 | return videojs.createTimeRange(0, this.currentTime());
231 | }
232 |
233 | supportsFullScreen() {
234 | return true;
235 | }
236 |
237 | preload() {}
238 | load() {}
239 |
240 | reset() {
241 | //Dispose of the IMA SDK
242 | this.adsManager?.stop();
243 | this.adsManager?.destroy();
244 | this.adsManager = null;
245 | this.err = null;
246 | this.cuePoints = [];
247 | this.currentAd = null;
248 | this.currentTime_ = 0;
249 | this.muted_ = false;
250 | this.ended_ = false;
251 | this.paused_ = false;
252 | this.contentTracker.previousTime = 0;
253 | this.contentTracker.currentTime = 0;
254 | this.contentTracker.duration = 0;
255 | this.contentTracker.seeking = false;
256 | this.adsLoader?.destroy();
257 | this.adsLoader = null;
258 | this.adDisplayContainer?.destroy();
259 | this.adDisplayContainer = null;
260 | }
261 |
262 | dispose() {
263 | this.reset();
264 | this.player_ = null; // allow object to be GCed
265 |
266 | //Needs to be called after the IMA SDK is destroyed, otherwise there will be a null reference exception
267 | super.dispose.call(this);
268 | }
269 |
270 | /* THESE METHODS ARE CALLED DURING SOURCE INITIALIZATION */
271 |
272 | handleLateInit_(contentInfo) {
273 | this.player_ = contentInfo.imaPlayer;
274 | this.source.contentMediaElement = contentInfo.mediaElement;
275 | this.source.adWillAutoPlay = contentInfo.autoplay;
276 | this.source.adWillPlayMuted = contentInfo.muted;
277 | this.muted_ = contentInfo.muted;
278 | this.volume_ = contentInfo.volume;
279 | this.resize(contentInfo);
280 | this.setSource(this.source, true);
281 | }
282 |
283 | initAdContainer() {
284 | this.adDisplayContainer = new google.ima.AdDisplayContainer(
285 | this.el_,
286 | this.source.contentMediaElement
287 | );
288 | this.setAdsLoader();
289 | }
290 |
291 | setScreenMode(isFullscreen) {
292 | this.screenMode = isFullscreen
293 | ? google.ima.ViewMode.FULLSCREEN
294 | : google.ima.ViewMode.NORMAL;
295 | }
296 |
297 | setAdsLoader() {
298 | this.adsLoader = new google.ima.AdsLoader(this.adDisplayContainer);
299 | this.adsLoader.getSettings().setLocale(this.source.locale);
300 | this.adsLoader
301 | .getSettings()
302 | .setDisableFlashAds(this.source.disableFlashAds);
303 | this.adsLoader
304 | .getSettings()
305 | .setDisableCustomPlaybackForIOS10Plus(
306 | this.source.disableCustomPlaybackForIOS10Plus
307 | );
308 | this.adsLoader.getSettings().setVpaidMode(this.source.vpaidMode);
309 | this.adsLoader.getSettings().setNumRedirects(this.source.numRedirects);
310 | this.adsLoader.getSettings().setPlayerType("videojs-ima-player");
311 | this.adsLoader.getSettings().setPlayerVersion(version);
312 | this.adsLoader
313 | .getSettings()
314 | .setAutoPlayAdBreaks(this.source.autoPlayAdBreaks);
315 |
316 | this.adsLoader.addEventListener(
317 | google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,
318 | this.onAdEvent.bind(this, this.onAdsManagerLoaded),
319 | false
320 | );
321 | this.adsLoader.addEventListener(
322 | google.ima.AdErrorEvent.Type.AD_ERROR,
323 | this.onAdEvent.bind(this, this.onAdsLoaderError),
324 | false
325 | );
326 | }
327 |
328 | requestAds() {
329 | if (!this.source.adTagUrl && !this.source.adsResponse) {
330 | return;
331 | }
332 | const adsRequest = new google.ima.AdsRequest();
333 | if (this.source.adTagUrl) {
334 | adsRequest.adTagUrl = this.source.adTagUrl;
335 | } else {
336 | adsRequest.adsResponse = this.source.adsResponse;
337 | }
338 | adsRequest.forceNonLinearFullSlot = this.source.forceNonLinearFullSlot;
339 | adsRequest.linearAdSlotWidth = this.width;
340 | adsRequest.linearAdSlotHeight = this.height;
341 | adsRequest.nonLinearAdSlotWidth =
342 | this.source.nonLinearWidth || adsRequest.linearAdSlotWidth;
343 | adsRequest.nonLinearAdSlotHeight =
344 | this.source.nonLinearHeight || adsRequest.linearAdSlotHeight / 3;
345 | adsRequest.setAdWillAutoPlay(this.source.adWillAutoPlay);
346 | adsRequest.setAdWillPlayMuted(this.source.adWillPlayMuted);
347 | this.adsLoader.requestAds(adsRequest);
348 | }
349 |
350 | setAdsManager(e) {
351 | this.adsRenderingSettings = new google.ima.AdsRenderingSettings();
352 | // this should be handled by contrib ads statefullnes
353 | //this.adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true;
354 | Object.assign(
355 | this.adsRenderingSettings,
356 | this.source.adsRenderingSettings || {}
357 | );
358 |
359 | this.adsManager = e.getAdsManager(
360 | this.contentTracker,
361 | this.adsRenderingSettings
362 | );
363 |
364 | this.adsManager.addEventListener(
365 | google.ima.AdErrorEvent.Type.AD_ERROR,
366 | this.onAdEvent.bind(this, this.onAdError)
367 | );
368 | this.adsManager.addEventListener(
369 | google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED,
370 | this.onAdEvent.bind(this, this.onContentPauseRequested)
371 | );
372 | this.adsManager.addEventListener(
373 | google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED,
374 | this.onAdEvent.bind(this, this.onContentResumeRequested)
375 | );
376 | this.adsManager.addEventListener(
377 | google.ima.AdEvent.Type.ALL_ADS_COMPLETED,
378 | this.onAdEvent.bind(this, this.onAllAdsCompleted)
379 | );
380 | this.adsManager.addEventListener(
381 | google.ima.AdEvent.Type.LOADED,
382 | this.onAdEvent.bind(this, this.onAdLoaded)
383 | );
384 | this.adsManager.addEventListener(
385 | google.ima.AdEvent.Type.STARTED,
386 | this.onAdEvent.bind(this, this.onAdStarted)
387 | );
388 | this.adsManager.addEventListener(
389 | google.ima.AdEvent.Type.CLICK,
390 | this.onAdEvent.bind(this, this.onAdClick)
391 | );
392 | this.adsManager.addEventListener(
393 | google.ima.AdEvent.Type.COMPLETE,
394 | this.onAdEvent.bind(this, this.onAdComplete)
395 | );
396 | this.adsManager.addEventListener(
397 | google.ima.AdEvent.Type.SKIPPED,
398 | this.onAdEvent.bind(this, this.onAdSkipped)
399 | );
400 | this.adsManager.addEventListener(
401 | google.ima.AdEvent.Type.PAUSED,
402 | // we wont mix player's pause event with this
403 | this.onAdPaused.bind(this)
404 | );
405 | this.adsManager.addEventListener(
406 | google.ima.AdEvent.Type.RESUMED,
407 | this.onAdEvent.bind(this, this.onAdResumed)
408 | );
409 | this.adsManager.addEventListener(
410 | google.ima.AdEvent.Type.VOLUME_CHANGED,
411 | this.onAdEvent.bind(this, this.onVolumeChanged)
412 | );
413 | this.adsManager.addEventListener(
414 | google.ima.AdEvent.Type.VOLUME_MUTED,
415 | this.onAdEvent.bind(this, this.onVolumeMuted)
416 | );
417 | this.adsManager.addEventListener(
418 | google.ima.AdEvent.Type.AD_PROGRESS,
419 | this.onAdEvent.bind(this, this.onAdProgress)
420 | );
421 |
422 | // additional events retriggered to ima player
423 | this.adsManager.addEventListener(
424 | google.ima.AdEvent.Type.AD_BREAK_READY,
425 | this.onAdEvent.bind(this, null)
426 | );
427 | this.adsManager.addEventListener(
428 | google.ima.AdEvent.Type.AD_METADATA,
429 | this.onAdEvent.bind(this, null)
430 | );
431 | this.adsManager.addEventListener(
432 | google.ima.AdEvent.Type.FIRST_QUARTILE,
433 | this.onAdEvent.bind(this, null)
434 | );
435 | this.adsManager.addEventListener(
436 | google.ima.AdEvent.Type.IMPRESSION,
437 | this.onAdEvent.bind(this, null)
438 | );
439 | this.adsManager.addEventListener(
440 | google.ima.AdEvent.Type.INTERACTION,
441 | this.onAdEvent.bind(this, null)
442 | );
443 | this.adsManager.addEventListener(
444 | google.ima.AdEvent.Type.LINEAR_CHANGED,
445 | this.onAdEvent.bind(this, null)
446 | );
447 | this.adsManager.addEventListener(
448 | google.ima.AdEvent.Type.LOG,
449 | this.onAdEvent.bind(this, null)
450 | );
451 | this.adsManager.addEventListener(
452 | google.ima.AdEvent.Type.SKIPPABLE_STATE_CHANGED,
453 | this.onAdEvent.bind(this, null)
454 | );
455 | this.adsManager.addEventListener(
456 | google.ima.AdEvent.Type.MIDPOINT,
457 | this.onAdEvent.bind(this, null)
458 | );
459 | this.adsManager.addEventListener(
460 | google.ima.AdEvent.Type.THIRD_QUARTILE,
461 | this.onAdEvent.bind(this, null)
462 | );
463 | this.adsManager.addEventListener(
464 | google.ima.AdEvent.Type.USER_CLOSE,
465 | this.onAdEvent.bind(this, null)
466 | );
467 |
468 | this.triggerReady();
469 | }
470 |
471 | initAdsManager() {
472 | try {
473 | this.adsManager.init(this.width, this.height, this.screenMode);
474 | this.adsManager.setVolume(!this.muted_ ? this.volume_ : 0);
475 | this.adDisplayContainer.initialize();
476 | } catch (adError) {
477 | this.onAdError(adError);
478 | }
479 | }
480 |
481 | start() {
482 | if (this.currentAd) {
483 | console.war("Ad warning: ad is already playing");
484 | return;
485 | }
486 |
487 | if (!this.hasStarted_) {
488 | this.triggerHasStartedEvents();
489 | }
490 |
491 | try {
492 | this.adsManager.start();
493 | } catch (e) {
494 | this.onAdError(e);
495 | }
496 | }
497 |
498 | /* TRIGGER HELPER */
499 |
500 | triggerHasStartedEvents() {
501 | this.trigger("canplay");
502 | this.trigger("loadedmetadata");
503 | this.trigger("volumechange");
504 | this.trigger("firstplay");
505 | this.trigger("play");
506 | this.trigger("playing");
507 | }
508 |
509 | /* THESE CUSTOM METHODS ARE CALLED DIRECTLY BY PLAYER */
510 |
511 | preroll() {
512 | this.contentHasStarted_ = true;
513 | if (this.adsManager) {
514 | this.initAdsManager();
515 | this.autoplay() && this.play();
516 | }
517 | }
518 |
519 | postroll() {
520 | if (!this.contentCompleted_) {
521 | this.contentCompleted_ = true;
522 | this.onContentCompleted();
523 | }
524 | }
525 |
526 | forceSkip() {
527 | if (!this.isLinearAd()) {
528 | return;
529 | }
530 |
531 | if (this.adsManager.getAdSkippableState()) {
532 | return this.adsManager.skip();
533 | }
534 |
535 | if (!this.cuePoints.length) {
536 | this.reset();
537 | this.onContentResumeRequested();
538 | return;
539 | }
540 |
541 | this.adsManager.discardAdBreak();
542 | }
543 |
544 | resize(dimensions) {
545 | this.width = dimensions.fullscreen
546 | ? window.screen.width
547 | : dimensions.width;
548 | this.height = dimensions.fullscreen
549 | ? window.screen.height
550 | : dimensions.height;
551 | this.setScreenMode(dimensions.fullscreen);
552 | if (this.adsManager) {
553 | this.adsManager.resize(this.width, this.height, this.screenMode);
554 | this.trigger("resize");
555 | }
556 | }
557 |
558 | /* THESE EVENT METHODS MOSTLY HANDLES ADS MANAGER */
559 |
560 | onOptionsChanged(options) {
561 | if (options) {
562 | this.options_ = Object.assign(this.source, options);
563 | }
564 | }
565 |
566 | onAdsLoaderError(e) {
567 | this.onAdError(e, "AdsLoader");
568 | }
569 |
570 | onAdError(e, source) {
571 | const type = `${source || "Ad"} error`;
572 | console.warn(
573 | `VIDEOJS: ${type}: ${e.getError?.().getMessage?.() || e.stack}`
574 | );
575 | const innerError = e.getError?.().getInnerError?.();
576 | if (innerError) {
577 | console.warn(
578 | `VIDEOJS: InnerAdError: ${
579 | innerError.getMessage?.() || innerError.stack
580 | }`
581 | );
582 | }
583 | this.trigger("adserror");
584 | }
585 |
586 | onAdsManagerLoaded(e) {
587 | this.setAdsManager(e);
588 | this.cuePoints = this.adsManager.getCuePoints();
589 | this.trigger("adsready", this.cuePoints);
590 | }
591 |
592 | onAdLoaded(e) {
593 | this.currentAd = e.getAd();
594 |
595 | let adPosition = 0,
596 | totalAds = 0;
597 |
598 | if (this.currentAd.getAdPodInfo && this.currentAd.getAdPodInfo()) {
599 | adPosition = this.currentAd.getAdPodInfo().getAdPosition();
600 | totalAds = this.currentAd.getAdPodInfo().getTotalAds();
601 | }
602 | this.trigger("adchange", {
603 | adPosition: adPosition,
604 | totalAds: totalAds,
605 | });
606 |
607 | this.isLinearAd()
608 | ? this.onLinearAdLoaded()
609 | : this.onNonLinearAdLoaded();
610 | }
611 |
612 | onLinearAdLoaded() {
613 | this.trigger("waiting");
614 | this.trigger("ratechange");
615 | this.trigger("durationchange");
616 | }
617 |
618 | onNonLinearAdLoaded() {}
619 |
620 | onContentPauseRequested() {
621 | var isJSAd =
622 | this.currentAd &&
623 | this.currentAd.getContentType() === "application/javascript";
624 | this.trigger(
625 | "linearadstarted",
626 | !isJSAd || this.source.showControlsForJSAds
627 | );
628 | this.trigger("waiting");
629 | }
630 |
631 | onContentResumeRequested() {
632 | // skip sdk nopostroll/nopreroll calls, we have our own
633 | this.trigger("linearadended");
634 | }
635 |
636 | onAdStarted() {
637 | this.isLinearAd()
638 | ? this.onLinearAdStarted()
639 | : this.onNonLinearStarted();
640 | }
641 |
642 | onLinearAdStarted() {
643 | this.trigger("playing");
644 | }
645 |
646 | onNonLinearStarted() {
647 | this.trigger("nonlinearadstarted");
648 | }
649 |
650 | onAdSkipped() {
651 | if (this.paused()) {
652 | this.onAdResumed();
653 | }
654 | this.onAdComplete();
655 | }
656 |
657 | onAdComplete() {
658 | this.isLinearAd() ? this.onLinearAdEnded() : this.onNonLinearAdEnded();
659 | this.currentAd = null;
660 | }
661 |
662 | onLinearAdEnded() {}
663 |
664 | onNonLinearAdEnded() {
665 | this.trigger("nonlinearadended");
666 | }
667 |
668 | onAllAdsCompleted() {
669 | this.ended_ = true;
670 | this.trigger("ended");
671 | this.reset();
672 | }
673 |
674 | onAdPaused() {
675 | this.paused_ = true;
676 | this.trigger("pause");
677 | }
678 |
679 | onAdProgress(e) {
680 | const { currentTime } = e.getAdData();
681 | this.currentTime_ = currentTime ?? 0;
682 | this.trigger({ type: "timeupdate", target: this });
683 | }
684 |
685 | onAdResumed() {
686 | this.paused_ = false;
687 | this.trigger("play");
688 | }
689 |
690 | onAdClick() {
691 | this.pause();
692 | }
693 |
694 | onVolumeChanged() {}
695 |
696 | onVolumeMuted() {}
697 |
698 | onContentCompleted() {
699 | return this.adsLoader?.contentComplete();
700 | }
701 |
702 | onAdEvent(callback, e) {
703 | this.player_.trigger(e);
704 | if (typeof callback === "function") {
705 | callback.call(this, e);
706 | }
707 | }
708 |
709 | // only helper shortcut method
710 | isLinearAd() {
711 | return this.adsManager && this.currentAd && this.currentAd.isLinear();
712 | }
713 | }
714 |
715 | Ima.prototype.featuresTimeupdateEvents = true;
716 |
717 | Ima.isSupported = function () {
718 | return true;
719 | };
720 |
721 | Ima.canPlaySource = function (source) {
722 | return this.canPlayType(source);
723 | };
724 |
725 | Ima.canPlayType = function (source) {
726 | return source && source.type === "video/ima";
727 | };
728 |
729 | videojs.registerTech("Ima", Ima);
730 |
--------------------------------------------------------------------------------
/src/ima-time-display.js:
--------------------------------------------------------------------------------
1 | import videojs from "video.js";
2 |
3 | const RemainingTimeDisplay = videojs.getComponent("RemainingTimeDisplay");
4 | const TimeDisplay = videojs.getComponent("TimeDisplay");
5 |
6 | class ImaRemainingTimeDisplay extends RemainingTimeDisplay {
7 | // modified version of TimeDisplay method
8 |
9 | createEl() {
10 | // prefix "-" in later versions of vjs7
11 | // we need to call grandparent
12 | return TimeDisplay.prototype.createEl.call(this);
13 | }
14 |
15 | updateTextNode_() {
16 | if (!this.contentEl_) {
17 | return;
18 | }
19 |
20 | while (this.contentEl_.firstChild) {
21 | this.contentEl_.removeChild(this.contentEl_.firstChild);
22 | }
23 |
24 | this.textNode_ = document.createTextNode(
25 | this.getRemainingTimeLabel() +
26 | (this.formattedTime_ || "-0:00").replace("-", "")
27 | );
28 | this.contentEl_.appendChild(this.textNode_);
29 | }
30 |
31 | getRemainingTimeLabel() {
32 | let podCount = ": ";
33 | if (this.player_.totalAds > 1) {
34 | podCount = ` (${this.player_.adPosition} ${this.options_.ofLabel} ${this.player_.totalAds}): `;
35 | }
36 | return this.options_.adLabel + podCount;
37 | }
38 | }
39 |
40 | videojs.registerComponent("imaRemainingTimeDisplay", ImaRemainingTimeDisplay);
41 |
--------------------------------------------------------------------------------
/test/webdriver/basic.mjs:
--------------------------------------------------------------------------------
1 | import { By, Browser, Builder, until } from "selenium-webdriver";
2 | import chrome from "selenium-webdriver/chrome.js";
3 | import firefox from "selenium-webdriver/firefox.js";
4 |
5 | const chromeOptions = new chrome.Options();
6 | chromeOptions.addArguments("--headless=new");
7 | chromeOptions.addArguments("--mute-audio");
8 |
9 | const firefoxOptions = new firefox.Options();
10 | firefoxOptions.addArguments("-headless");
11 |
12 | // TODO: firefox installed by snap on Ubuntu just doesn't work
13 | [Browser.CHROME].forEach((browserName) => {
14 | describe("Basic Tests " + browserName, function () {
15 | this.timeout(0);
16 | this.slow(15000);
17 |
18 | let driver;
19 |
20 | before(async function () {
21 | driver = new Builder()
22 | .forBrowser(browserName)
23 | .setChromeOptions(chromeOptions)
24 | .setFirefoxOptions(firefoxOptions)
25 | //.usingServer("")
26 | .build();
27 | return driver;
28 | });
29 |
30 | after(async function () {
31 | return driver.quit();
32 | });
33 |
34 | it("Displays ad UI " + browserName, async function () {
35 | await driver.get(
36 | "http://localhost:8000/test/webdriver/index.html?ad=linear"
37 | );
38 | let log = await driver.findElement(By.id("log"));
39 | await driver.wait(until.elementTextContains(log, "ready"), 10000);
40 | await driver.findElement(By.id("content_video")).click();
41 | await driver.wait(until.elementTextContains(log, "start"), 10000);
42 | await driver.wait(
43 | until.elementIsVisible(
44 | driver.findElement(By.css("#content_video_ima .vjs-control-bar"))
45 | ),
46 | 10000
47 | );
48 | });
49 |
50 | it("Hides ad player when ad ends " + browserName, async function () {
51 | await driver.get(
52 | "http://localhost:8000/test/webdriver/index.html?ad=linear"
53 | );
54 | let log = await driver.findElement(By.id("log"));
55 | await driver.wait(until.elementTextContains(log, "ready"), 10000);
56 | await driver.findElement(By.id("content_video")).click();
57 | await driver.wait(until.elementTextContains(log, "start"), 10000);
58 | await driver.wait(
59 | until.elementIsNotVisible(
60 | driver.findElement(By.id("content_video_ima"))
61 | ),
62 | 14000
63 | );
64 | await driver.sleep();
65 | });
66 |
67 | it("Plays content when ad ends " + browserName, async function () {
68 | await driver.get(
69 | "http://localhost:8000/test/webdriver/index.html?ad=linear"
70 | );
71 | let log = await driver.findElement(By.id("log"));
72 | await driver.wait(until.elementTextContains(log, "ready"), 11000);
73 | await driver.findElement(By.id("content_video")).click();
74 | await driver.wait(until.elementTextContains(log, "start"), 11000);
75 | await driver.wait(
76 | until.elementIsNotVisible(
77 | driver.findElement(By.id("content_video_ima"))
78 | ),
79 | 14000
80 | );
81 | await driver.wait(until.elementTextContains(log, "playing"), 1100);
82 | await driver.sleep();
83 | });
84 |
85 | it("Displays skip ad button " + browserName, async function () {
86 | await driver.get(
87 | "http://localhost:8000/test/webdriver/index.html?ad=skippable"
88 | );
89 | let log = await driver.findElement(By.id("log"));
90 | await driver.wait(until.elementTextContains(log, "ready"), 11000);
91 | await driver.findElement(By.id("content_video")).click();
92 | await driver.wait(until.elementTextContains(log, "start"), 11000);
93 | await driver
94 | .switchTo()
95 | .frame(
96 | driver.findElement(
97 | By.css(
98 | "#content_video_ima-ad-container > div:nth-child(1) > iframe"
99 | )
100 | )
101 | );
102 | let skipButton = await driver.findElement(
103 | By.css(
104 | "body > div.videoAdUi .videoAdUiSkipContainer.html5-stop-propagation > button"
105 | )
106 | );
107 | await driver.wait(until.elementIsVisible(skipButton), 11000);
108 | await driver.sleep();
109 | });
110 |
111 | it("VMAP: Preroll " + browserName, async function () {
112 | await driver.get(
113 | "http://localhost:8000/test/webdriver/index.html?ad=vmap_preroll"
114 | );
115 | let log = await driver.findElement(By.id("log"));
116 | await driver.wait(until.elementTextContains(log, "ready"), 11000);
117 | await driver.findElement(By.id("content_video")).click();
118 | await driver.wait(until.elementTextContains(log, "start"), 11000);
119 | await driver.wait(
120 | until.elementIsVisible(
121 | driver.findElement(By.id("content_video_ima-ad-container"))
122 | ),
123 | 10000
124 | );
125 | await driver.sleep();
126 | });
127 |
128 | it("VMAP: Midroll " + browserName, async function () {
129 | await driver.get(
130 | "http://localhost:8000/test/webdriver/index.html?ad=vmap_postroll"
131 | );
132 | let log = await driver.findElement(By.id("log"));
133 | await driver.wait(until.elementTextContains(log, "ready"), 11000);
134 | await driver.findElement(By.id("content_video")).click();
135 | await driver.executeScript(
136 | "window.videojs.players.content_video.currentTime(58)"
137 | );
138 | await driver.wait(until.elementTextContains(log, "start"), 11000);
139 | await driver.wait(
140 | until.elementIsVisible(
141 | driver.findElement(By.id("content_video_ima-ad-container"))
142 | ),
143 | 10000
144 | );
145 | await driver.sleep();
146 | });
147 |
148 | it("Nonlinear " + browserName, async function () {
149 | await driver.get(
150 | "http://localhost:8000/test/webdriver/index.html?ad=nonlinear"
151 | );
152 | let log = await driver.findElement(By.id("log"));
153 | await driver.wait(until.elementTextContains(log, "ready"), 11000);
154 | await driver.findElement(By.id("content_video")).click();
155 | await driver.wait(until.elementTextContains(log, "start"), 11000);
156 | await driver
157 | .switchTo()
158 | .frame(
159 | driver.findElement(
160 | By.css(
161 | "#content_video_ima-ad-container > div:nth-child(1) > iframe"
162 | )
163 | )
164 | );
165 | await driver.wait(
166 | until.elementIsVisible(
167 | driver.findElement(
168 | By.css("body .nonLinearContainer > .overlayContainer > img")
169 | )
170 | ),
171 | 10000
172 | );
173 | await driver.sleep();
174 | });
175 |
176 | it("Handles ad error 303: wrappers " + browserName, async function () {
177 | await driver.get(
178 | "http://localhost:8000/test/webdriver/index.html?ad=error_303"
179 | );
180 | let log = await driver.findElement(By.id("log"));
181 | await driver.wait(until.elementTextContains(log, "303"), 11000);
182 | await driver.sleep();
183 | });
184 | });
185 | });
186 |
--------------------------------------------------------------------------------
/test/webdriver/content/ads.js:
--------------------------------------------------------------------------------
1 | const onAdErrorEvent = function (event) {
2 | console.log(event);
3 | };
4 |
5 | const adTags = {
6 | linear: "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_ad_samples&sz=640x480&cust_params=sample_ct%3Dlinear&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator=",
7 | skippable:
8 | "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_preroll_skippable&sz=640x480&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator=",
9 | vmap_preroll:
10 | "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpreonly&ciu_szs=300x250%2C728x90&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&correlator=",
11 | vmap_postroll:
12 | "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpostonly&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&correlator=",
13 | nonlinear:
14 | "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/nonlinear_ad_samples&sz=480x70&cust_params=sample_ct%3Dnonlinear&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator=",
15 | error_303:
16 | "https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_ad_samples&sz=640x480&cust_params=sample_ct%3Dredirecterror&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator=",
17 | };
18 |
19 | const searchParams = new URLSearchParams(location.search);
20 | const adTagName = searchParams.get("ad");
21 | const player = videojs("content_video");
22 |
23 | player.ima({
24 | disableFlagAds: true,
25 | adTagUrl: adTags[adTagName],
26 | });
27 |
28 | player.ready(function () {
29 | const log = document.getElementById("log");
30 | log.innerHTML += "ready
";
31 | });
32 |
33 | const events = [
34 | google.ima.AdErrorEvent.Type.AD_ERROR,
35 | google.ima.AdEvent.Type.STARTED,
36 | ];
37 |
38 | for (let index = 0; index < events.length; index++) {
39 | player.ima.on(events[index], function (event) {
40 | const log = document.getElementById("log");
41 | const isImaError = event.type === google.ima.AdErrorEvent.Type.AD_ERROR;
42 | const msg = isImaError
43 | ? event.getError !== undefined
44 | ? event.getError().getVastErrorCode()
45 | : event.stack
46 | : event.type;
47 | log.innerHTML += msg + "
";
48 | });
49 | }
50 |
51 | player.on("playing", function (event) {
52 | const log = document.getElementById("log");
53 | log.innerHTML += event.type + "
";
54 | });
55 |
--------------------------------------------------------------------------------
/test/webdriver/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Video.js Test
5 |
6 |
7 |
8 |
9 |
10 |
11 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------