this repo has no description

better video/audio players, i hope

12Me21 c717ec6b 41cd015d

+114 -138
+32 -3
markup.css
··· 297 /** Media **/ 298 /***********/ 299 300 .M-image-wrapper { 301 aspect-ratio: 16/9; 302 contain: strict; ··· 315 background: black; 316 } 317 318 - .Markup media-player { 319 display: flex; 320 flex-flow: column; 321 max-width: 100%; ··· 328 padding-right: 3px; 329 } 330 331 - .M-audio-player, .M-video-player > .M-media-controls { 332 border: 2px solid var(--T-border-color); 333 background: slategray; 334 border-radius: 0 3px 3px 3px; ··· 336 width: max-content; 337 } 338 339 - .Markup media-player > * { 340 flex: none; 341 } 342
··· 297 /** Media **/ 298 /***********/ 299 300 + y12-audio { 301 + display: contents; 302 + } 303 + y12-audio > audio { 304 + display: block; 305 + width: 100%; 306 + } 307 + y12-audio > a { 308 + display: flex; 309 + width: 100%; 310 + align-items: center; 311 + padding: 3px 0.5rem; 312 + box-sizing: border-box; 313 + height: 40px; 314 + background: #555; 315 + color: silver; 316 + line-break: anywhere; 317 + text-decoration: none; 318 + } 319 + y12-audio > a > span { 320 + padding-left: 0.25rem; 321 + } 322 + 323 .M-image-wrapper { 324 aspect-ratio: 16/9; 325 contain: strict; ··· 338 background: black; 339 } 340 341 + y12-video > figure > span { 342 + z-index: 1; 343 + color: white; 344 + overflow-y: scroll; 345 + } 346 + 347 + y12-video { 348 display: flex; 349 flex-flow: column; 350 max-width: 100%; ··· 357 padding-right: 3px; 358 } 359 360 + y12-video > .M-media-controls { 361 border: 2px solid var(--T-border-color); 362 background: slategray; 363 border-radius: 0 3px 3px 3px; ··· 365 width: max-content; 366 } 367 368 + y12-video > * { 369 flex: none; 370 } 371
+3 -3
parse.js
··· 110 args.alt = rargs.named.alt 111 // todo: improve this 112 if (!type) { 113 - if (/[.](mp3|ogg|wav|m4a)\b/i.test(url)) 114 type = 'audio' 115 - else if (/[.](mp4|mkv|mov)\b/i.test(url)) 116 type = 'video' 117 else if (/^https?:[/][/](?:www[.])?(?:youtube.com[/]watch[?]v=|youtu[.]be[/]|youtube.com[/]shorts[/])[\w-]{11}/.test(url)) { 118 // todo: accept [start-end] args maybe? ··· 293 } 294 const ARG_REGEX = /.*?(?=])/y 295 const WORD_REGEX = /[^\s`^()+=\[\]{}\\|"';:,.<>/?!*]*/y 296 - const CODE_REGEX = /(?: *([-\w.+#$ ]+?) *(?![^\n]))?\n?([^]*?)(?:```|$)/y // ack 297 298 const parse=(text)=>{ 299 let tree = {type: 'ROOT', content: [], prev: 'all_newline'}
··· 110 args.alt = rargs.named.alt 111 // todo: improve this 112 if (!type) { 113 + if (/[.](mp3|ogg|wav|m4a|flac)\b/i.test(url)) 114 type = 'audio' 115 + else if (/[.](mp4|mkv|mov|webm)\b/i.test(url)) 116 type = 'video' 117 else if (/^https?:[/][/](?:www[.])?(?:youtube.com[/]watch[?]v=|youtu[.]be[/]|youtube.com[/]shorts[/])[\w-]{11}/.test(url)) { 118 // todo: accept [start-end] args maybe? ··· 293 } 294 const ARG_REGEX = /.*?(?=])/y 295 const WORD_REGEX = /[^\s`^()+=\[\]{}\\|"';:,.<>/?!*]*/y 296 + const CODE_REGEX = /(?: *([-\w.+#$ ]+?) *(?![^\n]))?\n?([^]*?)(?:\n?```|$)/y // ack 297 298 const parse=(text)=>{ 299 let tree = {type: 'ROOT', content: [], prev: 'all_newline'}
+43 -98
render.js
··· 121 error: 𐀶`<div class='error'><code>🕯error🕯</code>🕯message🕯<pre>🕯stack🕯`, 122 123 audio: function({url}) { 124 let e = this() 125 - let src = filter_url(url, 'audio') 126 - let c2 = e.lastChild 127 - let c1 = c2.previousSibling 128 - let [time, save, vol, volume] = c2.childNodes 129 - let [play, progress, , loop] = c1.childNodes 130 - save.href = src 131 - 132 - let audio 133 - function setup() { 134 - audio = document.createElement('audio') 135 - audio.preload = 'none' 136 - audio.src = src 137 - 138 - time.textContent = 'loading' 139 - 140 - volume.oninput = e=>{ 141 - audio.volume = +volume.value 142 - } 143 - function anim() { 144 - time.textContent = format_time(audio.currentTime)+" / "+format_time(audio.duration) 145 - progress.value = Math.round(audio.currentTime*10)/10 146 - } 147 - loop.onchange = e=>{ audio.loop = loop.checked } 148 - audio.onpause = e=>{ 149 - play.textContent = "▶️" 150 - } 151 - audio.onpause() 152 - audio.onplay = e=>{ 153 - play.textContent = "⏸️" 154 - } 155 - audio.onerror = e=>{ 156 - time.textContent = "Error" 157 - } 158 - function format_time(dur) { 159 - let s = dur 160 - let m = Math.floor(s / 60) 161 - s = s % 60 162 - return m+":"+(s+100).toFixed(1).substring(1) 163 - } 164 - audio.onvolumechange = e=>{ 165 - let volume = audio.volume 166 - vol.textContent = volume ? ["🔈", "🔉", "🔊"][volume*2.99|0] : "🔇" 167 - } 168 - if (volume.value==1) { 169 - volume.value = audio.volume 170 - audio.onvolumechange() 171 - } else { 172 - volume.oninput() 173 - } 174 - audio.ondurationchange = e=>{ 175 - progress.max = Math.round(audio.duration*10)/10 176 - time.textContent = format_time(audio.currentTime)+" / "+format_time(audio.duration) 177 - } 178 - audio.ontimeupdate = e=>{ 179 - anim() 180 - } 181 - progress.onchange = e=>{ 182 - audio.currentTime = progress.value 183 - } 184 } 185 - 186 - play.onclick = e=>{ 187 - if (!audio) 188 - setup() 189 - if (audio.paused) 190 - audio.play() 191 - else 192 - audio.pause() 193 - } 194 return e 195 - }.bind(𐀶` 196 - <media-player class='M-audio-player'> 197 - <div class='M-media-controls'> 198 - <button>▶️</button> 199 - <input type=range max=100 step=0.1 value=0> 200 - 🔁<input type=checkbox title=loop></input> 201 - </div> 202 - <div class='M-media-controls'> 203 - <span class='M-media-time'>‒‒/‒‒</span> 204 - <a target=_blank>💾</a> 205 - <span>🔊</span> 206 - <input type=range max=1 step=0.01 value=1 class='M-media-volume'> 207 - </div> 208 - </media-player> 209 - `), 210 video: function({url}) { 211 let e = this() 212 let media = document.createElement('video') 213 media.preload = 'none' 214 media.dataset.shrink = "video" 215 media.src = filter_url(url, 'video') 216 e.firstChild.append(media) 217 let cl = e.lastChild 218 let [play, progress, time] = cl.childNodes 219 play.onclick = e=>{ 220 - if (media.paused) { 221 media.play() 222 - //let e2 = new Event('videoclicked', {bubbles: true, cancellable: true}) 223 - //media.dispatchEvent(e2) 224 - } else 225 media.pause() 226 e.stopPropagation() 227 } 228 media.onresize = ev=>{ 229 media.onresize = null 230 media.parentNode.style.aspectRatio = media.videoWidth+"/"+media.videoHeight 231 media.parentNode.style.height = media.videoHeight+"px" 232 media.parentNode.style.width = media.videoWidth+"px" 233 } 234 media.ondurationchange = e=>{ 235 let s = media.duration 236 let m = Math.floor(s / 60) 237 s = s % 60 238 time.textContent = m+":"+(s+100).toFixed(2).substring(1) 239 } 240 media.ontimeupdate = e=>{ 241 - progress.value = media.currentTime / media.duration * 100 242 } 243 progress.onchange = e=>{ 244 - media.currentTime = progress.value/100 * media.duration 245 } 246 return e 247 }.bind(𐀶` 248 - <media-player class='M-video-player'> 249 - <div class='M-image-wrapper'></div> 250 - <div class='M-media-controls'> 251 - <button>Play</button> 252 - <input type=range max=100 value=0> 253 - <span>not loaded</span> 254 - </div> 255 - </media-player> 256 `), 257 258 italic: 𐀶`<i>`, ··· 315 let e = this() 316 e.firstChild.textContent = url 317 e.firstChild.href = url 318 - e.setAttribute('href', url) 319 return e 320 }.bind(𐀶`<youtube-embed><a target=_blank></a></youtube-embed>`), 321
··· 121 error: 𐀶`<div class='error'><code>🕯error🕯</code>🕯message🕯<pre>🕯stack🕯`, 122 123 audio: function({url}) { 124 + url = filter_url(url, 'audio') 125 let e = this() 126 + e.dataset.src = url 127 + e.onclick = ev=>{ 128 + ev.preventDefault() 129 + let e = ev.currentTarget 130 + let audio = document.createElement('audio') 131 + audio.controls = true 132 + audio.autoplay = true 133 + audio.src = e.dataset.src 134 + e.replaceChildren(audio) 135 + e.onclick = null 136 } 137 + let link = e.firstChild 138 + link.href = url 139 + link.title = url 140 + link.lastChild.textContent = url.replace(/.*[/]/, "…/") 141 return e 142 + }.bind(𐀶`<y12-audio><a>🎵️<span></span></a></y12-audio>`), 143 + 144 video: function({url}) { 145 let e = this() 146 let media = document.createElement('video') 147 + media.setAttribute('tabindex', 0) 148 media.preload = 'none' 149 media.dataset.shrink = "video" 150 media.src = filter_url(url, 'video') 151 e.firstChild.append(media) 152 + 153 let cl = e.lastChild 154 let [play, progress, time] = cl.childNodes 155 play.onclick = e=>{ 156 + if (media.paused) 157 media.play() 158 + else 159 media.pause() 160 e.stopPropagation() 161 } 162 + media.onpause = e=>{ 163 + play.textContent = "▶️" 164 + } 165 + media.onplay = e=>{ 166 + play.textContent = "⏸️" 167 + } 168 media.onresize = ev=>{ 169 media.onresize = null 170 media.parentNode.style.aspectRatio = media.videoWidth+"/"+media.videoHeight 171 media.parentNode.style.height = media.videoHeight+"px" 172 media.parentNode.style.width = media.videoWidth+"px" 173 } 174 + media.onerror = ev=>{ 175 + time.textContent = 'Error' 176 + } 177 media.ondurationchange = e=>{ 178 let s = media.duration 179 + progress.disabled = false 180 + progress.max = s 181 let m = Math.floor(s / 60) 182 s = s % 60 183 time.textContent = m+":"+(s+100).toFixed(2).substring(1) 184 } 185 media.ontimeupdate = e=>{ 186 + progress.value = media.currentTime 187 } 188 progress.onchange = e=>{ 189 + media.currentTime = progress.value 190 } 191 return e 192 }.bind(𐀶` 193 + <y12-video> 194 + <figure class='M-image-wrapper'></figure> 195 + <div class='M-media-controls'> 196 + <button>▶️</button> 197 + <input type=range min=0 max=1 step=any value=0 disabled> 198 + <span>not loaded</span> 199 + </div> 200 + </y12-video> 201 `), 202 203 italic: 𐀶`<i>`, ··· 260 let e = this() 261 e.firstChild.textContent = url 262 e.firstChild.href = url 263 + e.dataset.href = url 264 return e 265 }.bind(𐀶`<youtube-embed><a target=_blank></a></youtube-embed>`), 266
+36 -34
runtime.js
··· 19 </youtube-embed> 20 That way, it's still accessible if the custom element isn't installed. 21 **/ 22 - class YoutubeEmbedElement extends HTMLElement { 23 constructor() { 24 super() 25 this.attachShadow({mode: 'open'}) 26 let e = this.constructor.template() 27 for (let elem of e.querySelectorAll("[id]")) 28 - this["_"+elem.id] = elem 29 - this._link.onclick = e=>{ 30 - e.preventDefault() 31 this.show_youtube(true) 32 } 33 - this._close.onclick = e=>{ 34 this.show_youtube(false) 35 } 36 this.shadowRoot.append(e) 37 } 38 show_youtube(state) { 39 - this._close.hidden = !state 40 this.toggleAttribute('data-big', state) 41 - if (!this._iframe == !state) 42 return 43 if (state) { 44 - this._iframe = document.createElement('iframe') 45 let src = `https://www.youtube-nocookie.com/embed/${this._id}?autoplay=1&rel=0&modestbranding=1` 46 if (this._query) 47 src += `&${this._query}` 48 - this._iframe.src = src 49 - this._link.replaceWith(this._iframe) 50 } else { 51 - this._iframe.replaceWith(this._link) 52 - this._iframe.src = "about:blank" 53 - this._iframe = null 54 } 55 } 56 connectedCallback() { 57 - this.update_href(this.getAttribute('href')) 58 } 59 disconnectedCallback() { 60 this._id = null ··· 66 if (this._href == url) 67 return 68 this._href = url 69 - this._title.textContent = url 70 - this._author.textContent = "" 71 - this._link.href = url 72 73 let [, id, query] = /^https?:[/][/](?:www[.])?(?:youtube.com[/]watch[?]v=|youtu[.]be[/])([\w-]{11,})([&?].*)?$/.exec(url) 74 if (query) { ··· 85 // display video info 86 if (this._id == id) 87 return 88 - this._link.style.backgroundImage = `url(https://i.ytimg.com/vi/${id}/mqdefault.jpg)` 89 this._id = id 90 // only do one at a time 91 - let f = YoutubeEmbedElement.requests[id] 92 if (!f) { 93 // todo: cancel these when node is disconnected? 94 - YoutubeEmbedElement.requests[id] = f = fetch(`https://www.youtube.com/oembed?url=https%3A//youtube.com/watch%3Fv%3D${id}&format=json`).then(x=>x.json()).catch(x=>null) 95 } 96 f.then(data=>{ 97 if (this._id != id) 98 return // if the video changed 99 if (!data) 100 - data = {title: "unknown video"} 101 - this._title.textContent = data.title 102 - this._author.textContent = data.author_name 103 }) 104 } 105 attributeChangedCallback(name, old, value) { ··· 108 } 109 } 110 // intern these? 111 - YoutubeEmbedElement.requests = {} 112 - YoutubeEmbedElement.observedAttributes = ['href'] 113 { 114 let template = ([html])=>{ 115 let temp = document.createElement('template') 116 - temp.innerHTML = html.replace(/\s*\n\s*/g, "") 117 return document.importNode.bind(document, temp.content, true) 118 } 119 - YoutubeEmbedElement.template = template` 120 <a target=_blank id=link> 121 <cite id=caption> 122 <span id=title></span> ··· 124 <span id=author></span> 125 </cite> 126 </a> 127 - <button hidden id=close>❌</button> 128 <style> 129 :host { 130 - border: 2px solid gray; 131 display: flex !important; 132 --height: 135px; 133 - flex-direction: column; 134 } 135 :host([data-big]) { 136 --height: 270px; 137 } 138 #close { 139 - width: 25px 140 flex-shrink: 0; 141 } 142 iframe { ··· 149 height: var(--height); 150 padding: 4px; 151 overflow-y: auto; 152 - background-repeat: no-repeat; 153 - background-size: contain; 154 overflow-wrap: break-word; 155 white-space: pre-wrap; 156 flex-grow: 1; ··· 162 font-family: sans-serif; 163 display: inline; 164 } 165 #caption > div { 166 height: 5px; 167 } ··· 173 ` 174 } 175 176 - customElements.define('youtube-embed', YoutubeEmbedElement)
··· 19 </youtube-embed> 20 That way, it's still accessible if the custom element isn't installed. 21 **/ 22 + class Markup_YoutubeElement extends HTMLElement { 23 constructor() { 24 super() 25 this.attachShadow({mode: 'open'}) 26 let e = this.constructor.template() 27 for (let elem of e.querySelectorAll("[id]")) 28 + this["$"+elem.id] = elem 29 + this.$link.onclick = ev=>{ 30 + ev.preventDefault() 31 this.show_youtube(true) 32 } 33 + this.$close.onclick = ev=>{ 34 this.show_youtube(false) 35 } 36 this.shadowRoot.append(e) 37 } 38 show_youtube(state) { 39 + this.$close.hidden = !state 40 this.toggleAttribute('data-big', state) 41 + if (!this.$iframe == !state) 42 return 43 if (state) { 44 + this.$iframe = document.createElement('iframe') 45 let src = `https://www.youtube-nocookie.com/embed/${this._id}?autoplay=1&rel=0&modestbranding=1` 46 if (this._query) 47 src += `&${this._query}` 48 + this.$iframe.src = src 49 + this.$link.replaceWith(this.$iframe) 50 } else { 51 + this.$iframe.replaceWith(this.$link) 52 + this.$iframe.src = "about:blank" 53 + this.$iframe = null 54 } 55 } 56 connectedCallback() { 57 + this.update_href(this.dataset.href) 58 } 59 disconnectedCallback() { 60 this._id = null ··· 66 if (this._href == url) 67 return 68 this._href = url 69 + this.$title.textContent = url 70 + this.$author.textContent = "" 71 + this.$link.href = url 72 73 let [, id, query] = /^https?:[/][/](?:www[.])?(?:youtube.com[/]watch[?]v=|youtu[.]be[/])([\w-]{11,})([&?].*)?$/.exec(url) 74 if (query) { ··· 85 // display video info 86 if (this._id == id) 87 return 88 + this.$link.style.backgroundImage = `url(https://i.ytimg.com/vi/${id}/mqdefault.jpg)` 89 this._id = id 90 // only do one at a time 91 + let f = Markup_YoutubeElement.requests[id] 92 if (!f) { 93 // todo: cancel these when node is disconnected? 94 + Markup_YoutubeElement.requests[id] = f = fetch(`https://www.youtube.com/oembed?url=https%3A//youtube.com/watch%3Fv%3D${id}&format=json`).then(x=>x.json()).catch(x=>null) 95 } 96 f.then(data=>{ 97 if (this._id != id) 98 return // if the video changed 99 if (!data) 100 + data = {title: url, author_name: "(metadata request failed)"} 101 + this.$title.textContent = data.title 102 + this.$author.textContent = data.author_name 103 }) 104 } 105 attributeChangedCallback(name, old, value) { ··· 108 } 109 } 110 // intern these? 111 + Markup_YoutubeElement.requests = {} 112 + Markup_YoutubeElement.observedAttributes = ['data-href'] 113 { 114 let template = ([html])=>{ 115 let temp = document.createElement('template') 116 + temp.innerHTML = html.replace(/\s*?\n\s*/g, "") 117 return document.importNode.bind(document, temp.content, true) 118 } 119 + Markup_YoutubeElement.template = template` 120 <a target=_blank id=link> 121 <cite id=caption> 122 <span id=title></span> ··· 124 <span id=author></span> 125 </cite> 126 </a> 127 + <button hidden id=close>❌ close</button> 128 <style> 129 :host { 130 display: flex !important; 131 + border: 2px solid gray; 132 --height: 135px; 133 + flex-direction: column; 134 } 135 :host([data-big]) { 136 --height: 270px; 137 } 138 #close { 139 + /*width: 25px;*/ 140 flex-shrink: 0; 141 } 142 iframe { ··· 149 height: var(--height); 150 padding: 4px; 151 overflow-y: auto; 152 + background: no-repeat 0 / contain; 153 overflow-wrap: break-word; 154 white-space: pre-wrap; 155 flex-grow: 1; ··· 161 font-family: sans-serif; 162 display: inline; 163 } 164 + #caption > span { 165 + padding: 0 0.25rem; 166 + } 167 #caption > div { 168 height: 5px; 169 } ··· 175 ` 176 } 177 178 + customElements.define('youtube-embed', Markup_YoutubeElement)