PageSourceSearch

https://acu.edu/wp-content/themes/acu/ebenezer-hero/scripts/hero.js?ver=1790094187

js acu.edu collected 2026-09-24 08:37:41 UTC 25,963 bytes, 741 lines download raw bytes

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.