1/* ============================================================================ 2 * TFS 2987210 -- WCAG 2.2.2 "Pause, Stop, Hide" (Level A), Package D step D7. 3 * 4 * Pause/play wiring for the autoplay BACKGROUND VIDEO embeds -- the Hero 5 * widget (RPWebParts/Layout/Hero.ascx.cs) and the Background Video widget 6 * (RPWebParts/Gallery/BackgroundVideo.ascx.cs). Both offer the same three 7 * providers, and all three autoplay muted and loop forever with no user-facing 8 * stop, which is a straight SC 2.2.2 failure: the criterion covers any moving 9 * content that starts automatically and lasts more than five seconds. 10 * 11 * Each provider needs a different call, so this file normalises them behind 12 * one RPPauseControl (see pause-control.js): 13 * 14 * YouTube -- YT.Player instance -> pauseVideo() / playVideo(). The IFrame 15 * API is already loaded by both widgets (enablejsapi=1 is 16 * already on both embed URLs), so nothing new is requested. 17 * Vimeo -- Vimeo.Player instance -> pause() / play(). The Player SDK is 18 * NEW in this ticket; it is added server-side in the vimeo 19 * branch of each widget's code-behind, next to where Wistia 20 * already loads its own iframe API. 21 * Wistia -- Wistia's _wq queue -> video.pause() / video.play(). The iframe 22 * API is ALREADY loaded by both widgets
23 * (Hero.ascx.cs:256, BackgroundVideo.ascx.cs:189), so Wistia 24 * needed no new dependency -- confirmed rather than assumed. 25 * 26 * PLAYER READINESS. None of the three players exists at the moment this file 27 * runs; each becomes available through its own async callback. Rather than 28 * racing them, the control resolves its player lazily, at click time, through 29 * a caller-supplied accessor. A click that lands before the player is ready is 30 * a no-op on the video but still flips the button label -- so the two would 31 * desynchronise. To avoid that, the button is not attached at all until the 32 * player resolves. A background video with a broken player therefore shows no 33 * pause button, which is correct: there is nothing running to pause. 34 * 35 * MERGED MODE (options.merged). Hero and Background Video are the only two 36 * widgets in this repo where two independent moving mechanisms coexist: the 37 * FlexSlider slide rotation AND the video playback. Shipping a separate control 38 * for each is defensible under SC 2.2.2 but poor UX -- the criterion asks for 39 * *a* mechanism to stop the moving content, not one per mechanism. So when a 40 * caller reports that both are present, FlexSlider's own pausePlay control is 41 * suppressed at init (see hero.js and BackgroundVideo.ascx) and the single 42 * button built here drives BOTH. 43 * 44 * Merged mode inverts the readiness rule above, deliberately. The button is 45 * attached IMMEDIATELY rather than waiting on the player, because it always has 46 * real work to do -- stopping the rotation -- regardless of whether the video 47 * embed ever resolves. Deferring it would mean a blocked or broken embed left 48 * the slideshow with no control at all, since FlexSlider's was suppressed on 49 * the synchronous fact that a video is configured. The video half wires itself 50 * in the moment the player resolves, and if the user already paused by then, 51 * the resolve callback applies that pause to the video so the two cannot 52 * desynchronise. 53 * ==========================================================================*/ 54 55(function (window, $) { 56 'use strict'; 57 58 if (!$) { 59 return; 60 } 61 62 var VIMEO_SDK = 'https://player.vimeo.com/api/player.js'; 63 var POLL_INTERVAL = 150; 64 // 20s. Long enough for a slow third-party player on a cold cache, short 65 // enough that a genuinely broken embed stops polling. Hero.js has a 66 // cautionary example of the alternative: an un-cleared 100ms setInterval 67 // whose guard never passes, left spinning for the life of the page. 68 var POLL_TIMEOUT = 20000; 69 70 /** 71 * Poll until resolve() returns something truthy, then hand it to onReady. 72 * Gives up after POLL_TIMEOUT and always clears its own interval. 73 */ 74 function whenReady(resolve, onReady) { 75 var immediate; 76 try { 77 immediate = resolve(); 78 } catch (e) { 79 immediate = null; 80 } 81 if (immediate) { 82 onReady(immediate); 83 return; 84 } 85 86 var waited = 0; 87 var timer = window.setInterval(function () { 88 var value = null; 89 try { 90 value = resolve(); 91 } catch (e) { 92 value = null; 93 } 94 95 if (value) { 96 window.clearInterval(timer); 97 onReady(value); 98 return; 99 } 100 101 waited += POLL_INTERVAL; 102 if (waited >= POLL_TIMEOUT) { 103 window.clearInterval(timer); 104 } 105 }, POLL_INTERVAL); 106 } 107 108 function ensureVimeoSdk(callback) { 109 if (window.Vimeo && window.Vimeo.Player) { 110 callback(); 111 return; 112 } 113 // The code-behind emits the SDK <script> tag server-side, so normally 114 // this branch only waits for it. The injection below is a safety net 115 // for a cached page rendered before this ticket deployed. 116 if (!document.getElementById('rp-vimeo-sdk')) { 117 var tag = document.createElement('script'); 118 tag.id = 'rp-vimeo-sdk'; 119 tag.src = VIMEO_SDK; 120 document.getElementsByTagName('head')[0].appendChild(tag); 121 } 122 whenReady(function () { 123 return (window.Vimeo && window.Vimeo.Player) ? window.Vimeo : null; 124 }, function () { 125 callback(); 126 }); 127 } 128 129 /** 130 * options.scope (required) container selector, e.g. '#hero-background-video'. 131 * options.controlContainer (optional) where the button goes; defaults to 132 * the first .slides-video inside scope. 133 * options.source 'youtube' | 'vimeo' | 'wistia' 134 * options.getYouTubePlayer () => YT.Player, for source 'youtube' 135 * options.videoId Wistia hashed id, for source 'wistia' 136 * options.key de-dup key passed through to RPPauseControl 137 * options.merged true when this widget ALSO has a rotating 138 * FlexSlider and the caller has suppressed 139 * FlexSlider's own pausePlay control, so this one 140 * button must drive both. See MERGED MODE above. 141 * options.slider (required when merged) selector or jQuery for 142 * the .flexslider element to drive. 143 */ 144 function attach(options) { 145 options = options || {}; 146 147 var $scope = $(options.scope); 148 if (!$scope.length || !window.RPPauseControl) { 149 return; 150 } 151 152 // Attach to the banner container, NOT to the .slides-video <li>. Both widgets run 153 // their video slide inside a FlexSlider, and FlexSlider CLONES slides for its loop -- 154 // hero.js even has to delete a duplicated .slides-video.clone. A button appended to a 155 // slide would therefore be duplicated, or land on a clone that is currently hidden. 156 // The banner div is outside the cloned subtree and is already position:absolute in 157 // hero.css, so it is both stable and a valid positioning context for the overlay. 158 var containerSelector = options.controlContainer; 159 var $control = containerSelector ? $scope.find(containerSelector).first() 160 : $scope.find('.slides-banner').first(); 161 if (!$control.length) { 162 // #hero-background-video IS the .slides-banner element, so find() misses it. 163 $control = $scope.filter('.slides-banner').first(); 164 } 165 if (!$control.length) { 166 $control = $scope; 167 } 168 169 var source = (options.source || 'youtube').toLowerCase(); 170 var key = options.key || ('video-' + source); 171 172 // A caller that asks for merged mode without naming a slider has nothing 173 // to merge with. Treat that as the ordinary video-only control rather 174 // than building a button whose label promises slideshow control it 175 // cannot deliver. 176 var merged = !!options.merged && !!options.slider; 177 178 // The video handlers are mutable because in merged mode the button is 179 // built BEFORE the player resolves (see MERGED MODE in the file header), 180 // so its click handler has to read them at click time rather than close 181 // over them. No-ops until the player arrives. 182 var videoPause = function () { }; 183 var videoPlay = function () { }; 184 var control = null; 185 var built = false; 186 187 function getSlider() { 188 if (!options.slider) { 189 return null; 190 } 191 var slider = $(options.slider).first().data('flexslider'); 192 return (slider && typeof slider.pause === 'function' && 193 typeof slider.play === 'function') ? slider : null; 194 } 195 196 // Mirror FlexSlider's OWN pausePlay handler (jquery.flexslider.js:413-427), 197 // which sets manualPause/manualPlay either side of the call. Those flags are
198 // load-bearing, not decoration: BackgroundVideo's `after` force-resume checks 199 // !slider.manualPause before restarting rotation (BackgroundVideo.ascx:181, :209), 200 // and pauseOnHover resume checks both -- so a pause that skipped the flags 201 // would be silently undone on the next transition. 202 function sliderPause(slider) { 203 slider = slider || getSlider(); 204 if (!slider) { 205 return; 206 } 207 slider.manualPause = true; 208 slider.manualPlay = false; 209 try { slider.pause(); } catch (e) { } 210 } 211 212 function sliderPlay(slider) { 213 slider = slider || getSlider(); 214 if (!slider) { 215 return; 216 } 217 slider.manualPause = false; 218 slider.manualPlay = true; 219 try { slider.play(); } catch (e) { } 220 } 221 222 function build() { 223 built = true; 224 control = window.RPPauseControl.attach({ 225 container: $control, 226 overlay: true, 227 key: key, 228 // The merged name has to cover both mechanisms without implying it 229 // only reaches one of them. It is fixed at build time because both 230 // facts behind `merged` -- a video is configured, and the slideshow 231 // rotates -- are known synchronously by the caller; only the 232 // player's readiness is async, and that does not change the wording. 233 playingLabel: merged ? 'Pause slideshow and video' : 'Pause background video', 234 pausedLabel: merged ? 'Play slideshow and video' : 'Play background video', 235 onPause: function () { 236 if (merged) { sliderPause(); } 237 videoPause(); 238 }, 239 onPlay: function () { 240 if (merged) { sliderPlay(); } 241 videoPlay(); 242 } 243 }); 244 } 245 246 /** 247 * Called by each provider branch once its player resolves. 248 * 249 * Video-only mode: this is what gates the button's existence, exactly as 250 * before -- an unreachable player means no button, because there would be 251 * nothing it could stop. 252 * 253 * Merged mode: the button already exists, so this only wires the video 254 * half in. If the user pressed pause while the player was still 255 * resolving, apply that pause now -- otherwise the video would start 256 * playing under a button reading "Play ...". 257 */ 258 function setVideoHandlers(pauseFn, playFn) { 259 videoPause = pauseFn; 260 videoPlay = playFn; 261 262 if (!built) { 263 build(); 264 return; 265 } 266 if (control && control.isPaused()) { 267 pauseFn(); 268 } 269 } 270 271 if (merged) { 272 build(); 273 // The slider is resolved lazily rather than captured here: Background 274 // Video initialises its FlexSlider from an IntersectionObserver 275 // (BackgroundVideo.ascx:31, :115), so it can come into existence AFTER 276 // this button does. This poll exists purely for the state-sync case -- 277 // a slider that arrives while the control is already paused must start 278 // paused, not rotate under a button reading "Play ...". Clicks resolve 279 // the slider on their own through getSlider(). 280 whenReady(getSlider, function (slider) { 281 if (control && control.isPaused()) { 282 sliderPause(slider); 283 } 284 }); 285 } 286 287 if (source === 'vimeo') { 288 ensureVimeoSdk(function () { 289 var iframe = $scope.find('iframe').get(0); 290 if (!iframe) { 291 return; 292 } 293 var player; 294 try { 295 player = new window.Vimeo.Player(iframe); 296 } catch (e) { 297 return; 298 }
299 setVideoHandlers( 300 function () { try { player.pause(); } catch (e) { } }, 301 function () { try { player.play(); } catch (e) { } } 302 ); 303 }); 304 return; 305 } 306 307 if (source === 'wistia') { 308 window._wq = window._wq || []; 309 window._wq.push({ 310 id: options.videoId || '_all', 311 onReady: function (video) { 312 setVideoHandlers( 313 function () { try { video.pause(); } catch (e) { } }, 314 function () { try { video.play(); } catch (e) { } } 315 ); 316 } 317 }); 318 return; 319 } 320 321 // YouTube. The player handle is created by the widget's own 322 // onYouTubeIframeAPIReady callback; wait for it rather than assume it. 323 whenReady(function () { 324 var player = null; 325 if (typeof options.getYouTubePlayer === 'function') { 326 player = options.getYouTubePlayer(); 327 } 328 // A YT.Player object exists before its iframe is bound; 329 // pauseVideo only appears once the API has attached. 330 return (player && typeof player.pauseVideo === 'function') ? player : null; 331 }, function (player) { 332 setVideoHandlers( 333 function () { try { player.pauseVideo(); } catch (e) { } }, 334 function () { try { player.playVideo(); } catch (e) { } } 335 ); 336 }); 337 } 338 339 window.RPVideoPause = { attach: attach }; 340 341}(window, window.jQuery));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.