this repo has no description

better video/audio players, i hope

12Me21 c717ec6b 41cd015d

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