PageSourceSearch

https://transformedlifeoutreach.org/airo-video-slots.js

js transformedlifeoutreach.org collected 2026-10-08 22:44:06 UTC 10,168 bytes, 256 lines download raw bytes

1/**
2 * airo-video-slots.js — Runtime script for video slot patching.
3 *
4 * Fetches /airo-media.json to discover which media slots have mediaType 'video',
5 * then uses a MutationObserver to replace <img> elements referencing those slots
6 * with <video> elements (autoplay, muted, loop, playsInline).
7 *
8 * Loaded synchronously in <head> so the observer is active before React hydrates.
9 * Works in both dev and production modes — in dev mode the Vite plugin also handles
10 * video patching via HMR, but this script acts as a fallback for direct page loads
11 * (e.g. opening the preview link in a new tab without the builder parent frame).
12 *
13 * Safari notes:
14 * - muted/playsinline must be HTML attributes (set before src) + explicit play()
15 * - z-index:-1 fill videos paint behind the host's opaque background — clear it
16 */
17;(function () {
18  var SLOT_PREFIX_IMAGES = '/airo-assets/images/'
19  var SLOT_PREFIX_VIDEOS = '/airo-assets/videos/'
20  var BG_VIDEO_FILL_STYLE = 'position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:-1;pointer-events:none;'
21  var CLEAR_BG_ATTR = 'data-airo-bg-video-clear-bg'
22  var mediaTypes = {}
23
24  function configureAutoplayVideo(video) {
25    // Keep in lockstep with dev-tools/src/utils/autoplay-video.ts (parity test).
26    video.muted = true
27    video.defaultMuted = true
28    video.autoplay = true
29    video.loop = true
30    video.playsInline = true
31    video.preload = 'auto'
32    video.setAttribute('muted', '')
33    video.setAttribute('playsinline', '')
34    video.setAttribute('webkit-playsinline', '')
35    var kick = function () {
36      var p = video.play()
37      if (p && typeof p.catch === 'function') p.catch(function () {})
38    }
39    if (video.isConnected) {
40      // HAVE_CURRENT_DATA === 2 — mirror TS readyState gate
41      if (video.readyState >= 2) {
42        kick()
43      } else {
44        video.addEventListener('loadeddata', kick, { once: true })
45        // Also try immediately — Safari sometimes needs the call before loadeddata
46        kick()
47      }
48    } else if (typeof requestAnimationFrame === 'function') {
49      requestAnimationFrame(kick)
50    } else {
51      kick()
52    }
53  }
54
55  function prepareBackgroundVideoHost(el) {
56    var pos = window.getComputedStyle(el).position
57    if (pos === 'static') el.style.position = 'relative'
58    el.style.setProperty('background-color', 'transparent', 'important')
59    el.setAttribute(CLEAR_BG_ATTR, 'true')
60  }
61
62  function extractSlotPath(url) {
63    if (!url) return null
64    var prefixes = [SLOT_PREFIX_IMAGES, SLOT_PREFIX_VIDEOS]
65    for (var i = 0; i < prefixes.length; i++) {
66      var idx = url.indexOf(prefixes[i])
67      if (idx !== -1) {
68        var after = url.substring(idx + prefixes[i].length)
69        return after.split('?')[0]
70      }
71    }
72    return null
73  }
74
75  function patchImg(img) {
76    if (!img.src) return
77    if (img.getAttribute('data-airo-video-patched')) return
78    var slotPath = extractSlotPath(img.src)
79    if (!slotPath || mediaTypes[slotPath] !== 'video') return
80
81    // Remove any existing video for this slot to prevent duplicates after re-renders
82    var existing = img.parentNode && img.parentNode.querySelector('video[data-slot="' + slotPath + '"]')
83    if (existing) existing.remove()
84
85    var videoUrl = img.src.replace(SLOT_PREFIX_IMAGES, SLOT_PREFIX_VIDEOS)
86    var video = document.createElement('video')
87    video.className = img.className
88    video.style.cssText = img.style.cssText
89    if (img.width) video.width = img.width
90    if (img.height) video.height = img.height
91    video.setAttribute('data-airo-video', '')
92    video.setAttribute('data-slot', slotPath)
93    configureAutoplayVideo(video)
94    video.src = videoUrl
95
96    img.setAttribute('data-airo-video-patched', 'true')
97    img.style.display = 'none'
98    if (img.parentNode) {
99      img.parentNode.insertBefore(video, img.nextSibling)
100    }
101  }
102
103  function patchBgElement(el) {
104    var bgImage = window.getComputedStyle(el).backgroundImage
105    if (!bgImage || bgImage === 'none') return
106    if (bgImage.indexOf(SLOT_PREFIX_IMAGES) === -1 && bgImage.indexOf(SLOT_PREFIX_VIDEOS) === -1) return
107    var urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/)
108    if (!urlMatch || !urlMatch[1]) return
109    var slotPath = extractSlotPath(urlMatch[1])
110    if (!slotPath || mediaTypes[slotPath] !== 'video') return
111
112    // If already patched and video exists, just re-hide background (React may have restored it)
113    if (el.getAttribute('data-airo-video-bg-patched') === slotPath) {
114      el.style.backgroundImage = 'none'
115      return
116    }
117
118    // Remove any existing bg video to prevent duplicates
119    var existing = el.querySelector('video[data-airo-bg-video]')
120    if (existing) existing.remove()
121
122    el.style.backgroundImage = 'none'
123    el.setAttribute('data-airo-video-bg-patched', slotPath)
124    var videoUrl = urlMatch[1].replace(SLOT_PREFIX_IMAGES, SLOT_PREFIX_VIDEOS)
125    if (videoUrl.indexOf(SLOT_PREFIX_VIDEOS) === -1) {
126      videoUrl = SLOT_PREFIX_VIDEOS + slotPath
127    }
128    var video = document.createElement('video')
129    video.setAttribute('data-airo-bg-video', '')
130    video.setAttribute('data-slot', slotPath)
131    video.style.cssText = BG_VIDEO_FILL_STYLE
132    configureAutoplayVideo(video)
133    video.src = videoUrl
134    prepareBackgroundVideoHost(el)
135    el.insertBefore(video, el.firstChild)
136  }
137
138  function patchAll() {
139    document.querySelectorAll('img').forEach(patchImg)
140    // Check elements with inline background styles or already-patched elements
141    document.querySelectorAll('[style*="background"], [data-airo-video-bg-patched]').forEach(patchBgElement)
142    // Also scan all elements for CSS-class-based background-images referencing our slot prefixes.
143    // patchBgElement uses getComputedStyle internally and bails early if no matching URL is found,
144    // so this is safe but slightly more expensive — only run when we have video slots.
145    var hasVideoSlots = false
146    for (var k in mediaTypes) {
147      if (mediaTypes[k] === 'video') { hasVideoSlots = true; break }
148    }
149    if (hasVideoSlots) {
150      document.querySelectorAll('section, div, header, main, [class*="hero"], [class*="banner"], [class*="background"]').forEach(function (el) {
151        if (!el.getAttribute('data-airo-video-bg-patched') && !el.hasAttribute('style')) {
152          patchBgElement(el)
153        }
154      })
155    }
156  }
157
158  // Fetch manifest then start observing
159  fetch('/airo-media.json')
160    .then(function (res) {
161      if (!res.ok) return {}
162      return res.json()
163    })
164    .then(function (manifest) {
165      for (var key in manifest) {
166        if (manifest[key] && manifest[key].mediaType) {
167          mediaTypes[key] = manifest[key].mediaType
168        }
169      }
170      // Patch existing images
171      patchAll()
172      // Observe future DOM changes — childList for new nodes, attributes for style changes
173      var isPatching = false
174      var observer = new MutationObserver(function (mutations) {
175        if (isPatching) return
176        isPatching = true
177        try {
178        for (var i = 0; i < mutations.length; i++) {
179          var mutation = mutations[i]
180          if (mutation.type === 'childList') {
181            var added = mutation.addedNodes
182            for (var j = 0; j < added.length; j++) {
183              var node = added[j]
184              if (node instanceof HTMLImageElement) {
185                patchImg(node)
186              } else if (node instanceof HTMLElement) {
187                node.querySelectorAll('img').forEach(patchImg)
188                // Check added elements for background-image video slots
189                patchBgElement(node)
190                node.querySelectorAll('[style*="background"]').forEach(patchBgElement)
191              }
192            }
193          } else if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
194            var target = mutation.target
195            if (target instanceof HTMLElement) {
196              // Re-check: React may have re-rendered and set background-image back
197              // Remove stale patch flag so patchBgElement re-evaluates
198              if (target.getAttribute('data-airo-video-bg-patched')) {
199                var bg = target.style.backgroundImage
200                if (bg && bg !== 'none' && (bg.indexOf(SLOT_PREFIX_IMAGES) !== -1 || bg.indexOf(SLOT_PREFIX_VIDEOS) !== -1)) {
201                  target.removeAttribute('data-airo-video-bg-patched')
202                  patchBgElement(target)
203                }
204              } else {
205                patchBgElement(target)
206              }
207            }
208          }
209        }
210        } finally {
211          isPatching = false
212        }
213      })
214      observer.observe(document.documentElement, {
215        childList: true,
216        subtree: true,
217        attributes: true,
218        attributeFilter: ['style']
219      })
220
221      // Re-fetch manifest periodically to pick up changes (dev mode only — HMR may not work due to CORS)
222      // Use __AIRO_DEV_MODE__ flag set by dev-supervisor, falling back to localhost check
223      var isDevMode = window.__AIRO_DEV_MODE__ === true || location.hostname === 'localhost' || location.hostname === '127.0.0.1'
224      if (isDevMode) {
225        var pollFailures = 0
226        var pollIntervalId = setInterval(function () {
227          fetch('/airo-media.json').then(function (r) {
228            if (!r.ok) return {}
229            return r.json()
230          }).then(function (m) {
231            pollFailures = 0
232            var changed = false
233            for (var k in m) {
234              if (m[k] && m[k].mediaType && mediaTypes[k] !== m[k].mediaType) {
235                mediaTypes[k] = m[k].mediaType
236                changed = true
237              }
238            }
239            if (changed) patchAll()
240          }).catch(function (err) {
241            pollFailures++
242            if (pollFailures === 1) {
243              // eslint-disable-next-line no-console
244              console.warn('[airo-video-slots] manifest poll failed:', err.message || err)
245            }
246            if (pollFailures >= 5) {
247              clearInterval(pollIntervalId)
248            }
249          })
250        }, 3000)
251      }
252    })
253    .catch(function () {
254      // Manifest not available — no video slots to patch
255    })
256})()

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.