1/** 2 * Ebenezer hero media behavior. 3 * 4 * Videos start on their own only when the hero allows autoplay, reduced 5 * motion is not requested, and the connection can carry a video: Data Saver 6 * and anything slower than 4G or Wi-Fi keep the placeholder until the visitor 7 * presses play. In popup mode the ambient loop plays in the frame 8 * and the full video opens in a native dialog that restores ambient playback 9 * on close. In inline mode the placeholder image shows immediately, the full 10 * video buffers in the background once that image has loaded, and playback 11 * replaces the placeholder when the video can play through. Inline controls 12 * cover play/pause, mute, captions, and a transcript viewer, plus an expand 13 * control that opens the same video in the dialog at the inline position and 14 * hands that position back on close. 15 * 16 * The full video, inline or in the dialog, reports to Plausible as custom 17 * events: hero_video_play, hero_video_pause, hero_video_complete, 18 * hero_video_mute, hero_video_unmute, and hero_video_watch_time. Every event 19 * carries page_path, page_title, video_id, video_title, video_url, and 20 * video_duration props. The decorative ambient loop is not tracked. 21 */ 22(function () { 23 'use strict'; 24 25 var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 26 27 // The Network Information API is Chromium only, so a browser without it 28 // autoplays as before. Data Saver always wins. Wi-Fi and wired connections 29 // pass on their type; every other connection has to measure as 4G, so 3G 30 // and slower cellular keep the poster. 31 function connectionAllowsAutoplay(connection) { 32 if (!connection) { 33 return true; 34 } 35 if (connection.saveData === true) { 36 return false; 37 } 38 if (connection.type === 'wifi' || connection.type === 'ethernet') { 39 return true; 40 } 41 if (typeof connection.effectiveType === 'string') { 42 return connection.effectiveType === '4g'; 43 } 44 return true; 45 } 46 47 var slowConnection = !connectionAllowsAutoplay( 48 navigator.connection || navigator.mozConnection || navigator.webkitConnection 49 ); 50 51 function sourceUrl(video) { 52 var source = video.querySelector('source'); 53 return source ? source.src : video.currentSrc; 54 } 55 56 function sendVideoEvent(name, props) { 57 // Use the existing Plausible installation, or queue events until it loads. 58 window.plausible = window.plausible || function () { 59 (window.plausible.q = window.plausible.q || []).push(arguments); 60 }; 61 window.plausible(name, { props: props }); 62 } 63 64 // Plausible breaks a goal down by prop value and cannot sum one, so watch 65 // time also goes out as a range. 66 function watchTimeRange(seconds) { 67 if (seconds < 10) { 68 return 'under 10s'; 69 } 70 if (seconds < 30) { 71 return '10-29s'; 72 } 73 if (seconds < 60) { 74 return '30-59s'; 75 } 76 if (seconds < 120) { 77 return '60-119s'; 78 } 79 return '120s or more'; 80 } 81 82 // One tracker per hero. Inline mode plays one file in the frame and in the 83 // dialog, so both players feed the same completion and watch time. 84 function createVideoTracker() { 85 var reference = null; 86 var watched = 0; 87 var reported = 0; 88 var completed = false; 89 90 function videoProps(video) { 91 var props = { 92 page_path: window.location.pathname, 93 page_title: document.title, 94 video_id: video.getAttribute('data-ebenezer-video-id') || 'unknown', 95 video_title: video.getAttribute('data-ebenezer-video-title') || 'untitled', 96 video_url: sourceUrl(video) || 'unknown' 97 }; 98 if (isFinite(video.duration) && video.duration > 0) { 99 props.video_duration = String(Math.round(video.duration)); 100 } 101 return props; 102 } 103 104 // Sends the running total for this page view. The page can hide more 105 // than once, so a later report replaces an earlier, shorter one. 106 function reportWatchTime() { 107 var seconds = Math.floor(watched); 108 if (seconds < 1 || seconds === reported) { 109 return; 110 } 111 reported = seconds; 112 var props = videoProps(reference); 113 props.watch_seconds = String(seconds); 114 props.watch_time = watchTimeRange(seconds); 115 sendVideoEvent('hero_video_watch_time', props); 116 } 117 118 function watch(video, player) { 119 var nextPlay = null; 120 var quietPauses = 0; 121 var pausedWhileHidden = false;
122 var started = false; 123 var lastTime = video.currentTime; 124 var muted = video.muted || video.volume === 0; 125 126 if (!reference) { 127 reference = video; 128 document.addEventListener('visibilitychange', function () { 129 if (document.visibilityState === 'hidden') { 130 reportWatchTime(); 131 } 132 }); 133 window.addEventListener('pagehide', reportWatchTime); 134 } 135 136 function send(name, extra) { 137 var props = videoProps(video); 138 props.video_player = player; 139 props.video_position = String(Math.round(video.currentTime)); 140 if (props.video_duration) { 141 props.video_percent = String(Math.round((video.currentTime / video.duration) * 100)); 142 } 143 Object.keys(extra || {}).forEach(function (key) { 144 props[key] = extra[key]; 145 }); 146 sendVideoEvent(name, props); 147 } 148 149 function complete() { 150 if (completed) { 151 return; 152 } 153 completed = true; 154 // A loop is already back at its start by now, so report the end. 155 var extra = { video_percent: '100' }; 156 if (isFinite(video.duration)) { 157 extra.video_position = String(Math.round(video.duration)); 158 } 159 send('hero_video_complete', extra); 160 } 161 162 // A looping video never fires ended; its playhead jumps from the last 163 // moments back to the start instead. 164 function checkLoopRestart(from, to) { 165 if (video.loop && isFinite(video.duration) && from > video.duration - 2 && to < 2 && to < from) { 166 complete(); 167 } 168 } 169 170 video.addEventListener('play', function () { 171 var trigger = nextPlay || (pausedWhileHidden ? 'resume' : 'user'); 172 nextPlay = null; 173 pausedWhileHidden = false; 174 // A resume after an automated pause is not the visitor's action, 175 // unless it is the late first start of an autoplay that waited 176 // offscreen. 177 if (trigger === 'resume') { 178 if (started) { 179 return; 180 } 181 trigger = 'autoplay'; 182 } 183 started = true; 184 send('hero_video_play', { trigger: trigger }); 185 }); 186 187 video.addEventListener('pause', function () { 188 if (quietPauses > 0) { 189 quietPauses -= 1; 190 return; 191 } 192 // The browser pauses a video at its end; hero_video_complete covers it. 193 if (video.ended) { 194 return; 195 } 196 // Browsers pause a muted video in a hidden tab on their own and 197 // resume it when the visitor comes back. 198 if (document.hidden) { 199 pausedWhileHidden = true; 200 return; 201 } 202 send('hero_video_pause'); 203 }); 204 205 video.addEventListener('ended', complete); 206 207 video.addEventListener('seeking', function () { 208 checkLoopRestart(lastTime, video.currentTime); 209 lastTime = video.currentTime; 210 }); 211 212 video.addEventListener('timeupdate', function () { 213 var time = video.currentTime; 214 var step = time - lastTime; 215 // Playback moves the playhead a fraction of a second per update, so 216 // a larger step is a seek and adds no watch time. 217 if (step > 0 && step < 2 && !video.paused && !document.hidden) { 218 watched += step; 219 } 220 checkLoopRestart(lastTime, time); 221 lastTime = time; 222 }); 223 224 video.addEventListener('volumechange', function () { 225 var nowMuted = video.muted || video.volume === 0; 226 if (nowMuted === muted) { 227 return; 228 } 229 muted = nowMuted; 230 send(nowMuted ? 'hero_video_mute' : 'hero_video_unmute'); 231 }); 232 233 return { 234 // Names the cause of the next play event: 'autoplay', 'resume', or 235 // 'user'. Only a paused video fires one. 236 expectPlay: function (trigger) { 237 if (video.paused) { 238 nextPlay = trigger; 239 } 240 }, 241 // A blocked play() fires no play event, so its trigger must not 242 // stick to the visitor's own play later. 243 playBlocked: function () { 244 nextPlay = null; 245 }, 246 // Pauses for an offscreen video, the transcript, or the dialog 247 // handoff, which are not the visitor's own and send no event. 248 pauseQuietly: function () { 249 if (!video.paused) { 250 quietPauses += 1; 251 } 252 video.pause(); 253 } 254 }; 255 } 256 257 return { watch: watch }; 258 } 259 260 document.querySelectorAll('.acu-ebenezer-hero').forEach(function (hero) { 261 var ambient = hero.querySelector('.acu-ebenezer-hero__ambient'); 262 var toggle = hero.querySelector('[data-ebenezer-ambient-toggle]'); 263 var inlineVideo = hero.querySelector('[data-ebenezer-inline-video]'); 264 var inlineToggle = hero.querySelector('[data-ebenezer-inline-toggle]'); 265 var inlineMute = hero.querySelector('[data-ebenezer-inline-mute]'); 266 var inlineExpand = hero.querySelector('[data-ebenezer-inline-expand]'); 267 var openButton = hero.querySelector('[data-ebenezer-video-open]'); 268 var dialog = hero.querySelector('[data-ebenezer-video-dialog]'); 269 var dialogVideo = dialog ? dialog.querySelector('video') : null; 270 // The Autoplay field turns off the ambient loop and inline autoplay 271 // together; reduced motion and a slow or metered connection do the same 272 // on the visitor's side. 273 var autoplay = !reduceMotion && !slowConnection 274 && hero.getAttribute('data-ebenezer-autoplay') !== 'off'; 275 var videoTracker = createVideoTracker(); 276 var inlineTracking = inlineVideo ? videoTracker.watch(inlineVideo, 'inline') : null; 277 278 function playSafely(video, onBlocked) { 279 var result = video.play(); 280 if (result && typeof result.catch === 'function') { 281 result.catch(function (error) {
282 if (onBlocked && error && error.name === 'NotAllowedError') { 283 onBlocked(); 284 } 285 }); 286 } 287 } 288 289 // Inline playback the visitor did not ask for: the autoplay start, or a 290 // resume after an automated pause. 291 function playInline(trigger) { 292 inlineTracking.expectPlay(trigger); 293 playSafely(inlineVideo, inlineTracking.playBlocked); 294 } 295 296 function labelControl(control, label) { 297 control.setAttribute('aria-label', label); 298 control.setAttribute('title', label); 299 } 300 301 function syncToggle() { 302 if (!ambient || !toggle) { 303 return; 304 } 305 306 toggle.setAttribute('data-state', ambient.paused ? 'paused' : 'playing'); 307 labelControl(toggle, ambient.paused ? 'Play background video' : 'Pause background video'); 308 } 309 310 function syncInlineToggle() { 311 if (!inlineVideo || !inlineToggle) { 312 return; 313 } 314 315 inlineToggle.setAttribute('data-state', inlineVideo.paused ? 'paused' : 'playing'); 316 labelControl(inlineToggle, inlineVideo.paused ? 'Play video' : 'Pause video'); 317 } 318 319 function syncInlineMute() { 320 if (!inlineVideo || !inlineMute) { 321 return; 322 } 323 324 inlineMute.setAttribute('data-state', inlineVideo.muted ? 'muted' : 'unmuted'); 325 labelControl(inlineMute, inlineVideo.muted ? 'Unmute video' : 'Mute video'); 326 } 327 328 if (ambient) { 329 if (toggle) { 330 ambient.addEventListener('play', syncToggle); 331 ambient.addEventListener('pause', syncToggle); 332 toggle.addEventListener('click', function (event) { 333 event.stopPropagation(); 334 if (ambient.paused) { 335 playSafely(ambient); 336 } else { 337 ambient.pause(); 338 } 339 }); 340 syncToggle(); 341 } 342 343 if (autoplay) { 344 playSafely(ambient); 345 } 346 } 347 348 if (inlineVideo) { 349 var placeholder = hero.querySelector('[data-ebenezer-video-placeholder]'); 350 var inlineCaptions = hero.querySelector('[data-ebenezer-inline-captions]'); 351 var transcriptOpen = hero.querySelector('[data-ebenezer-transcript-open]'); 352 var transcriptDialog = hero.querySelector('[data-ebenezer-transcript-dialog]'); 353 var inlineMoreWrap = hero.querySelector('.acu-ebenezer-hero__inline-more'); 354 var inlineMore = hero.querySelector('[data-ebenezer-inline-more]'); 355 var inlineMenu = hero.querySelector('[data-ebenezer-inline-menu]'); 356 var inlineAutoPaused = false; 357 var inlineUserInteracted = false; 358 var inlineOffscreen = false; 359 var inlineBufferStarted = false; 360 361 // The placeholder stays up until frames are actually rendering, which 362 // also covers autoplay being blocked: the visitor keeps the image and 363 // the play control still works. 364 inlineVideo.addEventListener('playing', function () { 365 if (placeholder) { 366 placeholder.classList.add('acu-ebenezer-hero__placeholder--hidden'); 367 } 368 }); 369 370 function bufferInlineVideo() { 371 if (inlineBufferStarted) { 372 return; 373 } 374 inlineBufferStarted = true; 375 inlineVideo.preload = 'auto'; 376 if (inlineVideo.readyState === HTMLMediaElement.HAVE_NOTHING) { 377 inlineVideo.load(); 378 } 379 } 380 381 function autoplayInlineVideo() { 382 if (inlineUserInteracted || !inlineVideo.paused) { 383 return; 384 } 385 if (inlineOffscreen) { 386 inlineAutoPaused = true; 387 return; 388 } 389 playInline('autoplay'); 390 } 391 392 function onInlineReady() { 393 inlineVideo.removeEventListener('canplaythrough', onInlineReady); 394 autoplayInlineVideo(); 395 } 396 397 function startInlineAutoplay() { 398 if (inlineUserInteracted) { 399 return; 400 } 401 if (inlineVideo.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) { 402 autoplayInlineVideo(); 403 return; 404 } 405 inlineVideo.addEventListener('canplaythrough', onInlineReady); 406 bufferInlineVideo(); 407 } 408 409 if (inlineToggle) { 410 inlineVideo.addEventListener('play', syncInlineToggle); 411 inlineVideo.addEventListener('pause', syncInlineToggle); 412 inlineToggle.addEventListener('click', function () { 413 inlineAutoPaused = false;
414 inlineUserInteracted = true; 415 if (inlineVideo.paused) { 416 inlineTracking.expectPlay('user'); 417 playSafely(inlineVideo); 418 } else { 419 inlineVideo.pause(); 420 } 421 }); 422 syncInlineToggle(); 423 } 424 425 if (inlineMute) { 426 inlineVideo.addEventListener('volumechange', syncInlineMute); 427 inlineMute.addEventListener('click', function () { 428 inlineVideo.muted = !inlineVideo.muted; 429 }); 430 syncInlineMute(); 431 } 432 433 // Captions and the transcript live in a drop-up disclosure behind the 434 // three-dot button, so plain Tab and Escape cover its keyboard use. 435 function closeInlineMenu(focusTrigger) { 436 if (!inlineMenu || inlineMenu.hidden) { 437 return; 438 } 439 inlineMenu.hidden = true; 440 if (inlineMore) { 441 inlineMore.setAttribute('aria-expanded', 'false'); 442 if (focusTrigger) { 443 inlineMore.focus(); 444 } 445 } 446 } 447 448 // An empty menu offers nothing, so the three-dot button goes with it. 449 function syncInlineMenuVisibility() { 450 if (!inlineMoreWrap || !inlineMenu) { 451 return; 452 } 453 if (!inlineMenu.querySelector('button:not([hidden]), a:not([hidden])')) { 454 closeInlineMenu(false); 455 inlineMoreWrap.hidden = true; 456 } 457 } 458 459 if (inlineMore && inlineMenu) { 460 inlineMore.addEventListener('click', function () { 461 if (inlineMenu.hidden) { 462 inlineMenu.hidden = false; 463 inlineMore.setAttribute('aria-expanded', 'true'); 464 var firstItem = inlineMenu.querySelector('button:not([hidden]), a:not([hidden])'); 465 if (firstItem) { 466 firstItem.focus(); 467 } 468 } else { 469 closeInlineMenu(false); 470 } 471 }); 472 473 inlineMoreWrap.addEventListener('keydown', function (event) { 474 if (event.key === 'Escape' && !inlineMenu.hidden) { 475 event.stopPropagation(); 476 closeInlineMenu(true); 477 } 478 }); 479 480 document.addEventListener('click', function (event) { 481 if (!inlineMenu.hidden && !inlineMoreWrap.contains(event.target)) { 482 closeInlineMenu(false); 483 } 484 }); 485 } 486 487 if (inlineCaptions) { 488 var captionsTrack = inlineVideo.textTracks.length ? inlineVideo.textTracks[0] : null; 489 var captionsLabel = inlineCaptions.querySelector('[data-ebenezer-captions-label]'); 490 if (captionsTrack) { 491 captionsTrack.mode = 'hidden'; 492 var syncInlineCaptions = function () { 493 var showing = captionsTrack.mode === 'showing'; 494 inlineCaptions.setAttribute('data-state', showing ? 'on' : 'off'); 495 if (captionsLabel) { 496 captionsLabel.textContent = showing ? 'Hide captions' : 'Show captions'; 497 } 498 }; 499 inlineCaptions.addEventListener('click', function () { 500 captionsTrack.mode = captionsTrack.mode === 'showing' ? 'hidden' : 'showing'; 501 syncInlineCaptions(); 502 }); 503 // The browser can flip the track itself, for example from a saved 504 // captions preference, so keep the label tied to the real mode. 505 inlineVideo.textTracks.addEventListener('change', syncInlineCaptions); 506 syncInlineCaptions(); 507 } else { 508 inlineCaptions.hidden = true; 509 syncInlineMenuVisibility(); 510 } 511 } 512 513 if (transcriptOpen && transcriptDialog && typeof transcriptDialog.showModal === 'function') { 514 var resumeInline = false; 515 516 transcriptOpen.addEventListener('click', function () { 517 if (transcriptDialog.open) { 518 return; 519 } 520 closeInlineMenu(false); 521 resumeInline = !inlineVideo.paused; 522 if (resumeInline) { 523 inlineTracking.pauseQuietly(); 524 } 525 transcriptDialog.showModal(); 526 }); 527 528 var transcriptClose = transcriptDialog.querySelector('[data-ebenezer-transcript-close]'); 529 if (transcriptClose) { 530 transcriptClose.addEventListener('click', function () { 531 transcriptDialog.close(); 532 }); 533 } 534 535 transcriptDialog.addEventListener('click', function (event) { 536 if (event.target === transcriptDialog) { 537 transcriptDialog.close(); 538 } 539 }); 540 541 transcriptDialog.addEventListener('close', function () { 542 if (resumeInline) { 543 resumeInline = false;
544 playInline('resume'); 545 } 546 // The menu item that opened the dialog is hidden again, so move 547 // focus to its trigger instead of letting it fall to the body. 548 // Deferred, because the dialog restores focus after this event. 549 if (inlineMore && !inlineMoreWrap.hidden) { 550 window.setTimeout(function () { 551 inlineMore.focus(); 552 }, 0); 553 } 554 }); 555 } 556 557 // A video that cannot load leaves only the placeholder, so drop the 558 // playback controls and keep the transcript control reachable. 559 var inlineSource = inlineVideo.querySelector('source'); 560 function disableInlineControls() { 561 if (inlineToggle) { 562 inlineToggle.hidden = true; 563 } 564 if (inlineMute) { 565 inlineMute.hidden = true; 566 } 567 if (inlineCaptions) { 568 inlineCaptions.hidden = true; 569 } 570 if (inlineExpand) { 571 inlineExpand.hidden = true; 572 } 573 syncInlineMenuVisibility(); 574 } 575 inlineVideo.addEventListener('error', disableInlineControls); 576 if (inlineSource) { 577 inlineSource.addEventListener('error', disableInlineControls); 578 } 579 // Source failures can fire before these listeners attach, but 580 // NETWORK_NO_SOURCE is also a transient state while the browser is 581 // still picking a source, so only trust it once loading has settled. 582 window.setTimeout(function () { 583 if ( 584 inlineVideo.error 585 || ( 586 inlineVideo.networkState === HTMLMediaElement.NETWORK_NO_SOURCE 587 && inlineVideo.readyState === HTMLMediaElement.HAVE_NOTHING 588 ) 589 ) { 590 disableInlineControls(); 591 } 592 }, 3000); 593 594 // Pause offscreen playback and resume it on return, unless the 595 // visitor paused the video themselves. 596 if ('IntersectionObserver' in window) { 597 new IntersectionObserver(function (entries) { 598 entries.forEach(function (entry) { 599 inlineOffscreen = !entry.isIntersecting; 600 if (entry.isIntersecting) { 601 if (inlineAutoPaused) { 602 inlineAutoPaused = false; 603 playInline('resume'); 604 } 605 } else if (!inlineVideo.paused) { 606 inlineAutoPaused = true; 607 inlineTracking.pauseQuietly(); 608 } 609 }); 610 }).observe(inlineVideo); 611 } 612 613 // Buffering waits for the placeholder image so the video download does 614 // not compete with the hero's LCP image. Autoplay off, reduced motion, 615 // and a slow or metered connection never buffer: the placeholder stays 616 // until the visitor presses play. 617 if (autoplay) { 618 var placeholderImage = placeholder ? placeholder.querySelector('img') : null; 619 if (placeholderImage && !placeholderImage.complete) { 620 placeholderImage.addEventListener('load', startInlineAutoplay, { once: true }); 621 placeholderImage.addEventListener('error', startInlineAutoplay, { once: true }); 622 } else { 623 startInlineAutoplay(); 624 } 625 } 626 } 627 628 if (!dialog || typeof dialog.showModal !== 'function' || (!openButton && !inlineExpand)) { 629 return; 630 } 631 632 var media = hero.querySelector('.acu-ebenezer-hero__media'); 633 var resumeAmbient = false; 634 var resumeInlineAfterDialog = false; 635 636 // Every dialog play and pause is the visitor's own: the open and close 637 // buttons or the native controls. 638 if (dialogVideo) { 639 videoTracker.watch(dialogVideo, 'dialog'); 640 } 641 642 // Inline mode plays one file in both places, so the dialog picks up at 643 // the inline position and hands its own position back on close. Popup 644 // mode pairs an ambient loop with a separate full video, so no sync. 645 var dialogSharesInlineVideo = !!inlineVideo && !!dialogVideo 646 && sourceUrl(inlineVideo) === sourceUrl(dialogVideo); 647 648 // The dialog video starts with preload="none", so on a first open it can 649 // still be empty and the seek has to wait for its metadata.
650 function seekVideo(video, time) { 651 if (video.readyState >= HTMLMediaElement.HAVE_METADATA) { 652 video.currentTime = time; 653 return; 654 } 655 video.addEventListener('loadedmetadata', function () { 656 video.currentTime = time; 657 }, { once: true }); 658 } 659 660 // Fetching the dialog's metadata once inline playback is up makes the 661 // expand seek near instant. Only the metadata: a full preload would 662 // download the whole file a second time behind the inline copy. The 663 // raised preload doubles as the done flag for later playing events. 664 if (dialogSharesInlineVideo) { 665 inlineVideo.addEventListener('playing', function () { 666 if (dialog.open || dialogVideo.preload !== 'none') { 667 return; 668 } 669 dialogVideo.preload = 'metadata'; 670 if (dialogVideo.readyState === HTMLMediaElement.HAVE_NOTHING) { 671 dialogVideo.load(); 672 } 673 }); 674 } 675 676 function openDialog() { 677 if (dialog.open) { 678 return; 679 } 680 681 resumeAmbient = !!ambient && !ambient.paused; 682 if (ambient) { 683 ambient.pause(); 684 } 685 686 // The dialog plays the same video with sound, so inline playback 687 // pauses behind it and resumes when the dialog closes. 688 resumeInlineAfterDialog = !!inlineVideo && !inlineVideo.paused; 689 if (inlineVideo) { 690 inlineTracking.pauseQuietly(); 691 } 692 693 dialog.showModal(); 694 if (dialogVideo) { 695 if (dialogSharesInlineVideo) { 696 seekVideo(dialogVideo, inlineVideo.currentTime); 697 } 698 playSafely(dialogVideo); 699 } 700 } 701 702 // In popup mode the whole frame is clickable; inline mode opens the 703 // dialog only from its expand control. 704 if (openButton) { 705 (media || openButton).addEventListener('click', openDialog); 706 } 707 if (inlineExpand) { 708 inlineExpand.addEventListener('click', openDialog); 709 } 710 711 var closeButton = dialog.querySelector('[data-ebenezer-video-close]'); 712 if (closeButton) { 713 closeButton.addEventListener('click', function () { 714 dialog.close(); 715 }); 716 } 717 718 dialog.addEventListener('click', function (event) { 719 if (event.target === dialog) { 720 dialog.close(); 721 } 722 }); 723 724 dialog.addEventListener('close', function () { 725 if (dialogVideo) { 726 dialogVideo.pause(); 727 // A finished video hands the inline loop back to its start. 728 if (dialogSharesInlineVideo && dialogVideo.readyState >= HTMLMediaElement.HAVE_METADATA) { 729 inlineVideo.currentTime = dialogVideo.ended ? 0 : dialogVideo.currentTime; 730 } 731 } 732 if (resumeAmbient && ambient) { 733 playSafely(ambient); 734 } 735 if (resumeInlineAfterDialog && inlineVideo) { 736 resumeInlineAfterDialog = false; 737 playInline('resume'); 738 } 739 }); 740 }); 741})();
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.