PageSourceSearch

https://mobilechops.com/airo-video-slots.js

js mobilechops.com collected 2026-10-02 16:02:52 UTC 8,788 bytes, 219 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;(function () {
14  var SLOT_PREFIX_IMAGES = '/airo-assets/images/'
15  var SLOT_PREFIX_VIDEOS = '/airo-assets/videos/'
16  var mediaTypes = {}
17
18  function extractSlotPath(url) {
19    if (!url) return null
20    var prefixes = [SLOT_PREFIX_IMAGES, SLOT_PREFIX_VIDEOS]
21    for (var i = 0; i < prefixes.length; i++) {
22      var idx = url.indexOf(prefixes[i])
23      if (idx !== -1) {
24        var after = url.substring(idx + prefixes[i].length)
25        return after.split('?')[0]
26      }
27    }
28    return null
29  }
30
31  function patchImg(img) {
32    if (!img.src) return
33    if (img.getAttribute('data-airo-video-patched')) return
34    var slotPath = extractSlotPath(img.src)
35    if (!slotPath || mediaTypes[slotPath] !== 'video') return
36
37    // Remove any existing video for this slot to prevent duplicates after re-renders
38    var existing = img.parentNode && img.parentNode.querySelector('video[data-slot="' + slotPath + '"]')
39    if (existing) existing.remove()
40
41    var videoUrl = img.src.replace(SLOT_PREFIX_IMAGES, SLOT_PREFIX_VIDEOS)
42    var video = document.createElement('video')
43    video.src = videoUrl
44    video.autoplay = true
45    video.muted = true
46    video.loop = true
47    video.playsInline = true
48    video.className = img.className
49    video.style.cssText = img.style.cssText
50    if (img.width) video.width = img.width
51    if (img.height) video.height = img.height
52    video.setAttribute('data-airo-video', '')
53    video.setAttribute('data-slot', slotPath)
54
55    img.setAttribute('data-airo-video-patched', 'true')
56    img.style.display = 'none'
57    if (img.parentNode) {
58      img.parentNode.insertBefore(video, img.nextSibling)
59    }
60  }
61
62  function patchBgElement(el) {
63    var bgImage = window.getComputedStyle(el).backgroundImage
64    if (!bgImage || bgImage === 'none') return
65    if (bgImage.indexOf(SLOT_PREFIX_IMAGES) === -1 && bgImage.indexOf(SLOT_PREFIX_VIDEOS) === -1) return
66    var urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/)
67    if (!urlMatch || !urlMatch[1]) return
68    var slotPath = extractSlotPath(urlMatch[1])
69    if (!slotPath || mediaTypes[slotPath] !== 'video') return
70
71    // If already patched and video exists, just re-hide background (React may have restored it)
72    if (el.getAttribute('data-airo-video-bg-patched') === slotPath) {
73      el.style.backgroundImage = 'none'
74      return
75    }
76
77    // Remove any existing bg video to prevent duplicates
78    var existing = el.querySelector('video[data-airo-bg-video]')
79    if (existing) existing.remove()
80
81    el.style.backgroundImage = 'none'
82    el.setAttribute('data-airo-video-bg-patched', slotPath)
83    var videoUrl = urlMatch[1].replace(SLOT_PREFIX_IMAGES, SLOT_PREFIX_VIDEOS)
84    if (videoUrl.indexOf(SLOT_PREFIX_VIDEOS) === -1) {
85      videoUrl = SLOT_PREFIX_VIDEOS + slotPath
86    }
87    var video = document.createElement('video')
88    video.src = videoUrl
89    video.autoplay = true
90    video.muted = true
91    video.loop = true
92    video.playsInline = true
93    video.setAttribute('data-airo-bg-video', '')
94    video.setAttribute('data-slot', slotPath)
95    video.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:-1;'
96    var pos = window.getComputedStyle(el).position
97    if (pos === 'static') el.style.position = 'relative'
98    el.insertBefore(video, el.firstChild)
99  }
100
101  function patchAll() {
102    document.querySelectorAll('img').forEach(patchImg)
103    // Check elements with inline background styles or already-patched elements
104    document.querySelectorAll('[style*="background"], [data-airo-video-bg-patched]').forEach(patchBgElement)
105    // Also scan all elements for CSS-class-based background-images referencing our slot prefixes.
106    // patchBgElement uses getComputedStyle internally and bails early if no matching URL is found,
107    // so this is safe but slightly more expensive — only run when we have video slots.
108    var hasVideoSlots = false
109    for (var k in mediaTypes) {
110      if (mediaTypes[k] === 'video') { hasVideoSlots = true; break }
111    }
112    if (hasVideoSlots) {
113      document.querySelectorAll('section, div, header, main, [class*="hero"], [class*="banner"], [class*="background"]').forEach(function (el) {
114        if (!el.getAttribute('data-airo-video-bg-patched') && !el.hasAttribute('style')) {
115          patchBgElement(el)
116        }
117      })
118    }
119  }
120
121  // Fetch manifest then start observing
122  fetch('/airo-media.json')
123    .then(function (res) {
124      if (!res.ok) return {}
125      return res.json()
126    })
127    .then(function (manifest) {
128      for (var key in manifest) {
129        if (manifest[key] && manifest[key].mediaType) {
130          mediaTypes[key] = manifest[key].mediaType
131        }
132      }
133      // Patch existing images
134      patchAll()
135      // Observe future DOM changes — childList for new nodes, attributes for style changes
136      var isPatching = false
137      var observer = new MutationObserver(function (mutations) {
138        if (isPatching) return
139        isPatching = true
140        try {
141        for (var i = 0; i < mutations.length; i++) {
142          var mutation = mutations[i]
143          if (mutation.type === 'childList') {
144            var added = mutation.addedNodes
145            for (var j = 0; j < added.length; j++) {
146              var node = added[j]
147              if (node instanceof HTMLImageElement) {
148                patchImg(node)
149              } else if (node instanceof HTMLElement) {
150                node.querySelectorAll('img').forEach(patchImg)
151                // Check added elements for background-image video slots
152                patchBgElement(node)
153                node.querySelectorAll('[style*="background"]').forEach(patchBgElement)
154              }
155            }
156          } else if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
157            var target = mutation.target
158            if (target instanceof HTMLElement) {
159              // Re-check: React may have re-rendered and set background-image back
160              // Remove stale patch flag so patchBgElement re-evaluates
161              if (target.getAttribute('data-airo-video-bg-patched')) {
162                var bg = target.style.backgroundImage
163                if (bg && bg !== 'none' && (bg.indexOf(SLOT_PREFIX_IMAGES) !== -1 || bg.indexOf(SLOT_PREFIX_VIDEOS) !== -1)) {
164                  target.removeAttribute('data-airo-video-bg-patched')
165                  patchBgElement(target)
166                }
167              } else {
168                patchBgElement(target)
169              }
170            }
171          }
172        }
173        } finally {
174          isPatching = false
175        }
176      })
177      observer.observe(document.documentElement, {
178        childList: true,
179        subtree: true,
180        attributes: true,
181        attributeFilter: ['style']
182      })
183
184      // Re-fetch manifest periodically to pick up changes (dev mode only — HMR may not work due to CORS)
185      // Use __AIRO_DEV_MODE__ flag set by dev-supervisor, falling back to localhost check
186      var isDevMode = window.__AIRO_DEV_MODE__ === true || location.hostname === 'localhost' || location.hostname === '127.0.0.1'
187      if (isDevMode) {
188        var pollFailures = 0
189        var pollIntervalId = setInterval(function () {
190          fetch('/airo-media.json').then(function (r) {
191            if (!r.ok) return {}
192            return r.json()
193          }).then(function (m) {
194            pollFailures = 0
195            var changed = false
196            for (var k in m) {
197              if (m[k] && m[k].mediaType && mediaTypes[k] !== m[k].mediaType) {
198                mediaTypes[k] = m[k].mediaType
199                changed = true
200              }
201            }
202            if (changed) patchAll()
203          }).catch(function (err) {
204            pollFailures++
205            if (pollFailures === 1) {
206              // eslint-disable-next-line no-console
207              console.warn('[airo-video-slots] manifest poll failed:', err.message || err)
208            }
209            if (pollFailures >= 5) {
210              clearInterval(pollIntervalId)
211            }
212          })
213        }, 3000)
214      }
215    })
216    .catch(function () {
217      // Manifest not available — no video slots to patch
218    })
219})()

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.