├── .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 | --------------------------------------------------------------------------------