├── README.md ├── clappr.ads.js ├── index.html └── video.mp4 /README.md: -------------------------------------------------------------------------------- 1 | # Clappr-Ads 2 | Pre-roll, mid-roll and post-roll ads for the Clappr player 3 | 4 | ### Set Up 5 | ```JS 6 | var player = new Clappr.Player({ 7 | source: 'http://clappr.io/highline.mp4', 8 | plugins: [ClapprAds], 9 | ads: { 10 | preRoll: { 11 | src: ['video1.mp4', 'video2.mp4', 'video3.mp4'], 12 | skip: true, 13 | timeout: 5 14 | }, 15 | midRoll: { 16 | at: [10, 20, 35], 17 | src: ['video_1.mp4', 'video_2.mp4', 'video_3.mp4'], 18 | skip: true, 19 | timeout: 5 20 | }, 21 | postRoll: { 22 | src: 'video1.mp4' 23 | }, 24 | text: { 25 | wait: 'Wait % seconds...', 26 | skip: 'Skip >' 27 | } 28 | } 29 | }); 30 | ``` 31 | 32 | ### Mid-Roll 33 | 34 | #### Simplest Setup 35 | 36 | Ad will play once halfway through the video 37 | 38 | ```JS 39 | midRoll: { 40 | src: 'video.mp4', 41 | }, 42 | ``` 43 | 44 | #### Time Setup 45 | 46 | Specifying when ad will play 47 | 48 | ```JS 49 | midRoll: { 50 | at: 30, // 30 seconds 51 | src: 'video.mp4', 52 | }, 53 | ``` 54 | 55 | #### Random Videos Setup 56 | 57 | Ad will be randomly selected 58 | 59 | ```JS 60 | midRoll: { 61 | src: ['video1.mp4', 'video2.mp4', 'video3.mp4'] 62 | }, 63 | ``` 64 | 65 | #### Multiple Videos Setup 66 | 67 | Ads will play in different parts of the video 68 | 69 | ```JS 70 | midRoll: { 71 | at: [30, 60, 120], 72 | src: ['video1.mp4', 'video2.mp4', 'video3.mp4'] 73 | }, 74 | ``` 75 | 76 | #### Skipable Setup 77 | 78 | Ad will be skipable 79 | 80 | ```JS 81 | midRoll: { 82 | src: 'video.mp4', 83 | skip: true, 84 | timeout: 5 85 | }, 86 | ``` 87 | 88 | ### Options 89 | 90 | |Parameter|Type|Optional|Default Value|Description| 91 | |---|---|---|---|---| 92 | |ads.preRoll|object|true|-|-| 93 | |ads.preRoll.src|string/array|false|-|If array, it randomly selects a value| 94 | |ads.preRoll.skip|boolean|true|true|Shows skip button with countdown| 95 | |ads.preRoll.timeout|integer|true|5|Countdown time| 96 | |ads.midRoll|object|true|-|-| 97 | |ads.midRoll.at|integer/array|true|-|if unset, ad will play when video reaches 50%. If array, ad will play in the time (seconds) specified. If integer, ad will play once in the the time (seconds) specified| 98 | |ads.midRoll.src|string/array|false|-|If array, it randomly selects a value, unless it matches property 'at' in size, then it will play in order| 99 | |ads.midRoll.skip|boolean|true|true|Shows skip button with countdown| 100 | |ads.midRoll.timeout|integer|true|5|Countdown time| 101 | |ads.postRoll|object|true|-|-| 102 | |ads.postRoll.src|string/array|false|-|If array, it randomly selects a value| 103 | |ads.text|object|true|-|-| 104 | |ads.text.wait|string|true|Wait % seconds|Skip button wait text| 105 | |ads.text.skip|string|true|Skip >|Skip button action text| 106 | -------------------------------------------------------------------------------- /clappr.ads.js: -------------------------------------------------------------------------------- 1 | (function(w) { 2 | 3 | var Video = function(src, skip, timeout) { 4 | this.text = { 5 | wait: 'Wait % seconds...', 6 | skip: 'Skip >' 7 | }; 8 | this.onEnd = false; 9 | this.wrapper = this._initWrapper(); 10 | this.video = this._initVideo(src); 11 | this.wrapper.appendChild(this.video); 12 | this.muteButton = this._initMuteButton(); 13 | 14 | // if skip is true 15 | // add skip button 16 | if (skip) { 17 | this.skipButton = this._initSkipButton(timeout); 18 | this.wrapper.appendChild(this.skipButton); 19 | } 20 | }; 21 | 22 | Video.prototype._initWrapper = function() { 23 | var el = document.createElement('div'); 24 | el.style.display = 'block'; 25 | el.style.position = 'absolute'; 26 | el.style.width = '100%'; 27 | el.style.height = '100%'; 28 | el.style.top = '0px'; 29 | el.style.left = '0px'; 30 | el.style.zIndex = 10000; 31 | return el; 32 | }; 33 | 34 | Video.prototype._initVideo = function(src) { 35 | var el = document.createElement('video'); 36 | el.style.display = 'block'; 37 | el.style.position = 'absolute'; 38 | el.style.width = '100%'; 39 | el.style.height = '100%'; 40 | el.controls = false; 41 | el.src = src; 42 | el.addEventListener('ended', this._end.bind(this)); 43 | return el; 44 | }; 45 | 46 | Video.prototype._initSkipButton = function (timeout) { 47 | var el = document.createElement('button'); 48 | el.style.display = 'none'; 49 | el.style.position = 'absolute'; 50 | el.style.bottom = '45px'; 51 | el.style.right = '0px'; 52 | el.style.padding = '15px'; 53 | el.style.backgroundColor = '#000'; 54 | el.style.border = 'solid thin #000'; 55 | el.style.fontSize = '12px'; 56 | el.style.color = '#FFF'; 57 | el.style.right = '-1px'; 58 | el.disabled = true; 59 | el.addEventListener('click', this._end.bind(this)); 60 | this._skipButtonCountdown(el, timeout); 61 | return el; 62 | }; 63 | 64 | Video.prototype._initMuteButton = function() { 65 | var el = document.createElement('div'); 66 | el.style.position = 'absolute'; 67 | el.style.bottom = '145px'; 68 | el.style.right = '100px'; 69 | el.style.padding = '15px'; 70 | el.style.backgroundColor = '#000'; 71 | el.style.border = 'solid thin #000'; 72 | el.style.fontSize = '12px'; 73 | el.style.color = '#FFF'; 74 | el.innerText = 'Volume'; 75 | el.addEventListener('click', function () { 76 | this.video.muted = !this.video.muted; 77 | }.bind(this)); 78 | return el; 79 | }; 80 | 81 | Video.prototype._skipButtonCountdown = function(el, timeout) { 82 | var countDown = setInterval((function() { 83 | el.style.display = 'block'; 84 | if (timeout > 0) { 85 | el.innerHTML = this.text.wait.replace('%', timeout); 86 | timeout--; 87 | } else { 88 | el.innerHTML = this.text.skip; 89 | el.disabled = false; 90 | clearInterval(countDown); 91 | } 92 | }).bind(this), 1000); 93 | }; 94 | 95 | Video.prototype._end = function(evt) { 96 | // if click, prevent default 97 | if (evt) 98 | evt.preventDefault(); 99 | 100 | // remove video from the DOM 101 | this.wrapper.parentNode.removeChild(this.wrapper); 102 | 103 | // fire on end 104 | if (typeof(this.onEnd) === "function") { 105 | this.onEnd(); 106 | } 107 | }; 108 | 109 | Video.prototype.play = function() { 110 | this.video.play(); 111 | }; 112 | 113 | Video.prototype.pause = function() { 114 | this.video.pause(); 115 | }; 116 | 117 | Video.prototype.attachMuteButton = function () { 118 | this.wrapper.appendChild(this.muteButton); 119 | }; 120 | 121 | var ClapprAds = Clappr.UICorePlugin.extend({ 122 | _isAdPlaying: false, 123 | _hasPreRollPlayed: false, 124 | _hasPostRollPlayed: false, 125 | _preRoll: false, 126 | _midRoll: false, 127 | _postRoll: false, 128 | _videoText: {}, 129 | _rand: function (min, max) { 130 | return Math.floor(Math.random() * (max - min + 1)) + min; 131 | }, 132 | 133 | name: 'clappr_ads', 134 | 135 | initialize: function() { 136 | // get adplayer options 137 | if ('ads' in this._options) { 138 | if ('preRoll' in this._options.ads) { 139 | if ('src' in this._options.ads.preRoll) { 140 | this._preRoll = this._options.ads.preRoll; 141 | } else { 142 | throw "No source"; 143 | } 144 | } 145 | 146 | if ('midRoll' in this._options.ads) { 147 | if ('src' in this._options.ads.midRoll) { 148 | this._midRoll = this._options.ads.midRoll; 149 | 150 | // transform string src into an array 151 | if (typeof(this._midRoll.src) === "string") { 152 | this._midRoll.src = [this._midRoll.src]; 153 | } 154 | 155 | if ('at' in this._midRoll) { 156 | // if not an array, transform to array 157 | if (typeof(this._midRoll.at) != "object") { 158 | this._midRoll.at = [this._midRoll.at]; 159 | } 160 | } 161 | } else { 162 | throw "No source"; 163 | } 164 | } 165 | 166 | if ('postRoll' in this._options.ads) { 167 | if ('src' in this._options.ads.postRoll) { 168 | this._postRoll = this._options.ads.postRoll; 169 | } else { 170 | throw "No source"; 171 | } 172 | } 173 | 174 | if ('text' in this._options.ads) { 175 | var text = this._options.ads.text; 176 | if ('wait' in text) { 177 | this._videoText.wait = text.wait; 178 | } 179 | if ('skip' in text) { 180 | this._videoText.skip = text.skip; 181 | } 182 | } 183 | } 184 | 185 | this.bindEvents(); 186 | }, 187 | 188 | bindEvents: function() { 189 | // wait for core to be ready 190 | this.listenTo(this.core, Clappr.Events.CORE_READY, (function() { 191 | // get container 192 | var container = this.core.getCurrentContainer(); 193 | // listeners 194 | container.listenTo(container.playback, Clappr.Events.PLAYBACK_PLAY, this._onPlaybackPlay.bind(this, container)); 195 | container.listenTo(container.playback, Clappr.Events.PLAYBACK_TIMEUPDATE, this._onPlaybackTimeUpdate.bind(this, container)); 196 | container.listenTo(container.playback, Clappr.Events.PLAYBACK_ENDED, this._onPlaybackEnd.bind(this)); 197 | }).bind(this)); 198 | }, 199 | 200 | _onPlaybackPlay: function(container) { 201 | // if ad is playing, pause 202 | // otherwise, start pre-roll 203 | if (this._isAdPlaying) { 204 | container.playback.pause(); 205 | } else { 206 | // pre-roll will not run if played before or unset 207 | if (!this._preRoll || this._hasPreRollPlayed) 208 | return; 209 | 210 | this.playPreRoll(container); 211 | } 212 | }, 213 | 214 | _onPlaybackTimeUpdate: function(container) { 215 | // fetch current time and duration 216 | var current = container.currentTime; 217 | var duration = container.getDuration(); 218 | 219 | if (this._midRoll) { 220 | var atTimes; 221 | if ('at' in this._midRoll) { 222 | atTimes = this._midRoll.at; 223 | } else { 224 | atTimes = [Math.floor(duration / 2)]; 225 | } 226 | 227 | var inAtTimes = false, at, index; 228 | for (var i = 0; i < atTimes.length; i++) { 229 | at = atTimes[i]; 230 | if (Math.floor(current) == at) { 231 | index = i; 232 | inAtTimes = true; 233 | } 234 | } 235 | 236 | if (inAtTimes) { 237 | if (this._midRoll.at.length === this._midRoll.src.length) { 238 | this.playMidRoll(container, index); 239 | } else { 240 | this.playMidRoll(container); 241 | } 242 | } 243 | } 244 | 245 | // post-roll will not run if played before 246 | if (this._postRoll && !this._hasPostRollPlayed && current) { 247 | // if the video is in it's end, play post-roll 248 | if (Math.round(current * 1000) == Math.round(duration * 1000)) { 249 | this.playPostRoll(container); 250 | } 251 | } 252 | }, 253 | 254 | _onPlaybackEnd: function() { 255 | this._isAdPlaying = false; 256 | this._hasPreRollPlayed = false; 257 | }, 258 | 259 | playPreRoll: function(container) { 260 | // bail if ad is playing 261 | if (this._isAdPlaying) 262 | return; 263 | 264 | // if src is an array 265 | // select randomly one of the videos 266 | var src; 267 | if (typeof(this._preRoll.src) === "object") { 268 | src = this._preRoll.src[this._rand(0, this._preRoll.src.length - 1)]; 269 | } else { 270 | src = this._preRoll.src; 271 | } 272 | 273 | // pause playback 274 | container.playback.pause(); 275 | 276 | // initialize video 277 | video = new Video(src, this._preRoll.skip, this._preRoll.timeout); 278 | video.onEnd = this._onPreRollEnd.bind(this, video, container.playback); 279 | 280 | // video text 281 | if ('wait' in this._videoText) { 282 | video.text.wait = this._videoText.wait; 283 | } 284 | 285 | if ('skip' in this._videoText) { 286 | video.text.skip = this._videoText.skip; 287 | } 288 | 289 | // add mute button 290 | if (this._preRoll.muteButton) { 291 | video.attachMuteButton(); 292 | } 293 | 294 | // render video 295 | container.$el.append(video.wrapper); 296 | video.play(); 297 | 298 | // call user's callback if it is set 299 | if ('onPlay' in this._preRoll) { 300 | this._preRoll.onPlay(this._preRoll, { position: 'preroll' }); 301 | } 302 | 303 | // make sure pre-roll wont play again 304 | this._hasPreRollPlayed = true; 305 | }, 306 | 307 | playMidRoll: function(container, index) { 308 | // bail if ad is playing 309 | if (this._isAdPlaying) 310 | return; 311 | 312 | this._isAdPlaying = true; 313 | 314 | // pause playback 315 | container.playback.pause(); 316 | 317 | // go to next second 318 | // to prevent a midroll loop 319 | container.playback.seek(parseInt(Math.floor(container.currentTime + 1))); 320 | 321 | var src; 322 | 323 | // if index was not set 324 | // select source randomly 325 | // otherwise get source index 326 | if (index === undefined) { 327 | src = this._midRoll.src[this._rand(0, this._midRoll.src.length - 1)]; 328 | } else { 329 | src = this._midRoll.src[index]; 330 | } 331 | 332 | // initialize video 333 | video = new Video(src, this._midRoll.skip, this._midRoll.timeout); 334 | 335 | // add mute button 336 | if (this._midRoll.muteButton) { 337 | video.attachMuteButton(); 338 | } 339 | 340 | // render video 341 | container.$el.append(video.wrapper); 342 | video.play(); 343 | 344 | // call user's callback if it is set 345 | if ('onPlay' in this._midRoll) { 346 | this._midRoll.onPlay(this._midRoll, { position: 'preroll' }); 347 | } 348 | 349 | video.onEnd = (function () { 350 | this._isAdPlaying = false; 351 | }).bind(this); 352 | 353 | }, 354 | 355 | playPostRoll: function(container) { 356 | // bail if ad is playing 357 | if (this._isAdPlaying) 358 | return; 359 | 360 | this._isAdPlaying = true; 361 | 362 | // prevent multiple calls whilst running 363 | this._hasPostRollPlayed = true; 364 | 365 | // pause playback 366 | container.playback.pause(); 367 | 368 | // if src is an array 369 | // select randomly one of the videos 370 | var src; 371 | if (typeof(this._postRoll.src) === "object") { 372 | src = this._postRoll.src[this._rand(0, this._postRoll.src.length - 1)]; 373 | } else { 374 | src = this._postRoll.src; 375 | } 376 | 377 | // initialize video 378 | video = new Video(src); 379 | 380 | // add mute button 381 | if (this._postRoll.muteButton) { 382 | video.attachMuteButton(); 383 | } 384 | 385 | // render video 386 | container.$el.append(video.wrapper); 387 | video.play(); 388 | 389 | // call user's callback if it is set 390 | if ('onPlay' in this._postRoll) { 391 | this._postRoll.onPlay(this._postRoll, { position: 'preroll' }); 392 | } 393 | 394 | video.onEnd = (function () { 395 | this._isAdPlaying = false; 396 | }).bind(this); 397 | }, 398 | 399 | _onPreRollEnd: function(video, playback) { 400 | this._isAdPlaying = false; 401 | setTimeout(function() { playback.play(); }, 100); 402 | }, 403 | }); 404 | 405 | w.ClapprAds = ClapprAds; 406 | 407 | })(window); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |