← Commit history

adom-video: optional JSON manifest src (chapters/captions on private pages); fetch without credentials

John Lauer ·656aa8fac6 ·7d ago ·parent 1df1bc2
2 files changed +24−10
widgets.json+1
@@ -162,6 +162,7 @@   "element": "adom-video",   "attrs": [    "video",+   "src",    "title",    "poster",    "chapters",
widgets/video-widget.js+23−10
@@ -7,8 +7,9 @@ // captions (WebVTT or SRT, drawn by the player so + / - can resize them), speed and loop in a // settings menu, picture-in-picture, fullscreen. Self-contained: Shadow DOM, no dependencies. //-// Attributes: video (required; `src` also works outside the registry, but the wiki inlines a `src`-// file into the widget frame, so a registered tag must use `video`), title, poster, chapters (JSON [{t,title}] or a WebVTT chapter+// Attributes: video (required; never pass a video as `src`: the wiki inlines a registered tag's `src`+// file into the frame), src (optional JSON manifest {video, title, poster, chapters, captions}, which+// the wiki inlines, so chapters and captions work on private pages too), title, poster, chapters (JSON [{t,title}] or a WebVTT chapter // file), captions (.vtt or .srt), start (seconds or 1m30s), loop, muted, autoplay. // // Inside a wiki widget frame the browser blocks fullscreen (the frame carries no allow=@@ -271,14 +272,26 @@     // absolute URL for a wiki path (the frame is opaque-origin, so resolve against the real location)     _abs(u) { try { return new URL(u, location.href).href; } catch { return u; } } +    // a JSON manifest (the wiki inlines a registered tag's `src` file as a child script):+    // { "video": "...", "title", "poster", "chapters": [{t,title}] or a path, "captions": a path or WebVTT text }+    _manifest() {+      if (this._man !== undefined) return this._man;+      this._man = null;+      const el = this.querySelector('script[type="application/json"]');+      if (el) { try { const j = JSON.parse(el.textContent); if (j && typeof j === 'object' && !Array.isArray(j)) this._man = j; } catch {} }+      return this._man;+    }+    _opt(n) { const a = this.getAttribute(n); if (a != null && a !== '') return a; const m = this._manifest(); return m && m[n] != null ? m[n] : null; }+     _load() {       this._loaded = true;-      const src = this.getAttribute('video') || this.getAttribute('src');+      const m = this._manifest();+      const src = this.getAttribute('video') || (m && m.video) || (m ? null : this.getAttribute('src'));       const v = this.v;-      this.$('.top').textContent = this.getAttribute('title') || '';+      this.$('.top').textContent = this._opt('title') || '';       if (!src) return this._error('No video: add <code>video="/OWNER/SLUG/render/video.mp4"</code> to the tag.');       this.$('.err').classList.remove('on');-      if (this.getAttribute('poster')) v.poster = this._abs(this.getAttribute('poster'));+      if (this._opt('poster')) v.poster = this._abs(this._opt('poster'));       v.loop = this.hasAttribute('loop');       v.muted = this.hasAttribute('muted');       this._startAt = parseT(this.getAttribute('start') || (location.hash.match(/[#&]t=([^&]+)/) || [])[1]);@@ -295,13 +308,13 @@     _error(html) { const e = this.$('.err'); e.innerHTML = `<div>${html}</div>`; e.classList.add('on'); this.p.classList.remove('wait'); }      async _fetchText(u) {-      const r = await fetch(this._abs(u), { credentials: 'include' }).catch(() => null);+      const r = await fetch(this._abs(u)).catch(() => null);       return r && r.ok ? r.text() : null;     }     async _chapters() {-      const u = this.getAttribute('chapters'); this.chapters = [];+      const u = this._opt('chapters'); this.chapters = [];       if (u) {-        const txt = await this._fetchText(u);+        const txt = typeof u === 'string' ? await this._fetchText(u) : JSON.stringify(u);         if (txt) {           try { const j = JSON.parse(txt); this.chapters = (Array.isArray(j) ? j : j.chapters || []).map((c) => ({ t: parseT(c.t ?? c.time ?? c.start), title: String(c.title ?? c.name ?? '') })); }           catch { this.chapters = parseCues(txt).map((c) => ({ t: c.start, title: c.text.replace(/\n/g, ' ') })); }@@ -312,8 +325,8 @@       this._segments(); this._tick();     }     async _captions() {-      const u = this.getAttribute('captions'); this.cues = [];-      if (u) { const txt = await this._fetchText(u); if (txt) this.cues = parseCues(txt); }+      const u = this._opt('captions'); this.cues = [];+      if (u) { const txt = /-->/.test(u) ? u : await this._fetchText(u); if (txt) this.cues = parseCues(txt); }       this.$('[data-k=cc]').hidden = !this.cues.length;       this.capOn = this.cues.length > 0 && this.capOn;       this._caps(); this._sync();