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.