PageSourceSearch

https://www.hinrichfoundation.com/script/lite-yt-embed.js

js hinrichfoundation.com collected 2026-10-02 03:01:56 UTC 10,376 bytes, 241 lines download raw bytes

1/**
2 * A lightweight youtube embed. Still should feel the same to the user, just MUCH faster to initialize and paint.
3 *
4 * Thx to these as the inspiration
5 *   https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html
6 *   https://autoplay-youtube-player.glitch.me/
7 *
8 * Once built it, I also found these:
9 *   https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube (👍👍)
10 *   https://github.com/Daugilas/lazyYT
11 *   https://github.com/vb/lazyframe
12 */
13class LiteYTEmbed extends HTMLElement {
14   connectedCallback () {
15      this.videoId = this.getAttribute('videoid')
16
17      let playBtnEl = this.querySelector('.lyt-playbtn,.lty-playbtn')
18      // A label for the button takes priority over a [playlabel] attribute on the custom-element
19      this.playLabel = (playBtnEl && playBtnEl.textContent.trim()) || this.getAttribute('playlabel') || 'Play'
20
21      this.dataset.title = this.getAttribute('title') || ""
22
23      /**
24       * Lo, the youtube poster image!  (aka the thumbnail, image placeholder, etc)
25       *
26       * See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md
27       */
28      if (!this.style.backgroundImage) {
29         this.style.backgroundImage = `url("https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg")`
30         this.upgradePosterImage()
31      }
32
33      // Set up play button, and its visually hidden label
34      if (!playBtnEl) {
35         playBtnEl = document.createElement('button')
36         playBtnEl.type = 'button'
37         // Include the mispelled 'lty-' in case it's still being used. https://github.com/paulirish/lite-youtube-embed/issues/65
38         playBtnEl.classList.add('lyt-playbtn', 'lty-playbtn')
39         this.append(playBtnEl)
40      }
41      if (!playBtnEl.textContent) {
42         const playBtnLabelEl = document.createElement('span')
43         playBtnLabelEl.className = 'lyt-visually-hidden'
44         playBtnLabelEl.textContent = this.playLabel
45         playBtnEl.append(playBtnLabelEl)
46      }
47
48      this.addNoscriptIframe()
49
50      // for the PE pattern, change anchor's semantics to button
51      if (playBtnEl.nodeName === 'A') {
52         playBtnEl.removeAttribute('href')
53         playBtnEl.setAttribute('tabindex', '0')
54         playBtnEl.setAttribute('role', 'button')
55         // fake button needs keyboard help
56         playBtnEl.addEventListener('keydown', e => {
57            if (e.key === 'Enter' || e.key === ' ') {
58               e.preventDefault()
59               this.activate()
60            }
61         })
62      }
63
64      // On hover (or tap), warm up the TCP connections we're (likely) about to use.
65      this.addEventListener('pointerover', LiteYTEmbed.warmConnections, { once: true })
66      this.addEventListener('focusin', LiteYTEmbed.warmConnections, { once: true })
67
68      // Once the user clicks, add the real iframe and drop our play button
69      // TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time
70      //   We'd want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003
71      this.addEventListener('click', this.activate)
72
73      // Chrome & Edge desktop have no problem with the basic YouTube Embed with ?autoplay=1
74      // However Safari desktop and most/all mobile browsers do not successfully track the user gesture of clicking through the creation/loading of the iframe,
75      // so they don't autoplay automatically. Instead we must load an additional 2 sequential JS files (1KB + 165KB) (un-br) for the YT Player API
76      // TODO: Try loading the the YT API in parallel with our iframe and then attaching/playing it. #82
77      this.needsYTApi = this.hasAttribute("js-api") || navigator.vendor.includes('Apple') || navigator.userAgent.includes('Mobi')
78   }
79
80   /**
81    * Add a <link rel={preload | preconnect} ...> to the head
82    */
83   static addPrefetch (kind, url, as) {
84      const linkEl = document.createElement('link')
85      linkEl.rel = kind
86      linkEl.href = url
87      if (as) {
88         linkEl.as = as
89      }
90      document.head.append(linkEl)
91   }
92
93   /**
94    * Begin pre-connecting to warm up the iframe load
95    * Since the embed's network requests load within its iframe,
96    *   preload/prefetch'ing them outside the iframe will only cause double-downloads.
97    * So, the best we can do is warm up a few connections to origins that are in the critical path.
98    *
99    * Maybe `<link rel=preload as=document>` would work, but it's unsupported: http://crbug.com/593267
100    * But TBH, I don't think it'll happen soon with Site Isolation and split caches adding serious complexity.
101    */
102   static warmConnections () {
103      if (LiteYTEmbed.preconnected) return
104
105      // The iframe document and most of its subresources come right off youtube.com
106      LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com')
107      // The botguard script is fetched off from google.com
108      LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com')
109
110      // Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling.
111      LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net')
112      LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net')
113
114      LiteYTEmbed.preconnected = true
115   }
116
117   fetchYTPlayerApi () {
118      if (window.YT || (window.YT && window.YT.Player)) return
119
120      this.ytApiPromise = new Promise((res, rej) => {
121         var el = document.createElement('script')
122         el.src = 'https://www.youtube.com/iframe_api'
123         el.async = true
124         el.onload = _ => {
125            YT.ready(res)
126         }
127         el.onerror = rej
128         this.append(el)
129      })
130   }
131
132   /** Return the YT Player API instance. (Public L-YT-E API) */
133   async getYTPlayer () {
134      if (!this.playerPromise) {
135         await this.activate()
136      }
137
138      return this.playerPromise
139   }
140
141   async addYTPlayerIframe () {
142      this.fetchYTPlayerApi()
143      await this.ytApiPromise
144
145      const videoPlaceholderEl = document.createElement('div')
146      this.append(videoPlaceholderEl)
147
148      const paramsObj = Object.fromEntries(this.getParams().entries())
149
150      this.playerPromise = new Promise(resolve => {
151         let player = new YT.Player(videoPlaceholderEl, {
152            width: '100%',
153            videoId: this.videoId,
154            playerVars: paramsObj,
155            events: {
156               'onReady': event => {
157                  event.target.playVideo()
158                  resolve(player)
159               }
160            }
161         })
162      })
163   }
164
165   // Add the iframe within <noscript> for indexability discoverability. See https://github.com/paulirish/lite-youtube-embed/issues/105
166   addNoscriptIframe () {
167      const iframeEl = this.createBasicIframe()
168      const noscriptEl = document.createElement('noscript')
169      // Appending into noscript isn't equivalant for mysterious reasons: https://html.spec.whatwg.org/multipage/scripting.html#the-noscript-element
170      noscriptEl.innerHTML = iframeEl.outerHTML
171      this.append(noscriptEl)
172   }
173
174   getParams () {
175      const params = new URLSearchParams(this.getAttribute('params') || [])
176      params.append('autoplay', '1')
177      params.append('playsinline', '1')
178      return params
179   }
180
181   async activate () {
182      if (this.classList.contains('lyt-activated')) return
183      this.classList.add('lyt-activated')
184
185      if (this.needsYTApi) {
186         return this.addYTPlayerIframe(this.getParams())
187      }
188
189      const iframeEl = this.createBasicIframe()
190      this.append(iframeEl)
191
192      // Set focus for a11y
193      iframeEl.focus()
194   }
195
196   createBasicIframe () {
197      const iframeEl = document.createElement('iframe')
198      iframeEl.width = 560
199      iframeEl.height = 315
200      // No encoding necessary as [title] is safe. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#:~:text=Safe%20HTML%20Attributes%20include
201      iframeEl.title = this.playLabel
202      iframeEl.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
203      iframeEl.allowFullscreen = true
204      // Required by Youtube to fix Error 153
205      iframeEl.referrerPolicy = 'strict-origin-when-cross-origin'
206      // AFAIK, the encoding here isn't necessary for XSS, but we'll do it only because this is a URL
207      // https://stackoverflow.com/q/64959723/89484
208      iframeEl.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId)}?${this.getParams().toString()}`
209      return iframeEl
210   }
211
212   /**
213    * In the spirit of the `lowsrc` attribute and progressive JPEGs, we'll upgrade the reliable
214    * poster image to a higher resolution one, if it's available.
215    * Interestingly this sddefault webp is often smaller in filesize, but we will still attempt it second
216    * because getting _an_ image in front of the user if our first priority.
217    *
218    * See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md for more details
219    */
220   upgradePosterImage () {
221      // Defer to reduce network contention.
222      setTimeout(() => {
223         const webpUrl = `https://i.ytimg.com/vi_webp/${this.videoId}/sddefault.webp`
224         const img = new Image()
225         img.fetchPriority = 'low' // low priority to reduce network contention
226         img.referrerpolicy = 'origin' // Not 100% sure it's needed, but https://github.com/ampproject/amphtml/pull/3940
227         img.src = webpUrl
228         img.onload = e => {
229            // A pretty ugly hack since onerror won't fire on YouTube image 404. This is (probably) due to
230            // Youtube's style of returning data even with a 404 status. That data is a 120x90 placeholder image.
231            // … per "annoying yt 404 behavior" in the .md
232            const noAvailablePoster = e.target.naturalHeight == 90 && e.target.naturalWidth == 120
233            if (noAvailablePoster) return
234
235            this.style.backgroundImage = `url("${webpUrl}")`
236         }
237      }, 100)
238   }
239}
240// Register custom element
241customElements.define('lite-youtube', LiteYTEmbed)

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.