← Commit history
README.md+2−2
@@ -23,7 +23,7 @@ The same player as the wiki's `::adom-video` widget (adom/widgets): YouTube's lo | **Captions** | `--captions subs.vtt` (or .srt), drawn by the player: C toggles, + / - resize, O / W text and background opacity | | **Speed** | 0.25x to 4x in the settings menu or with `<` `>`; press and hold the video for 2x | | **Frame step** | `,` `.` at the measured frame rate, with a pill showing frame N of total and the exact time |-| **Keyboard** | YouTube's shortcuts: Space or K, J / L, arrows, ↑ ↓ volume, M, 0 to 9, Home / End, Ctrl+arrows chapters, F or double-click fullscreen, T theater, I picture-in-picture, R loop, Shift+N / Shift+P through the library, media keys, ? for the list |+| **Keyboard** | YouTube's shortcuts: Space or K, J / L, arrows, ↑ ↓ volume, M, 0 to 9, Home / End, Ctrl+arrows chapters, F or double-click fullscreen, I picture-in-picture, R loop, Shift+N / Shift+P through the library, media keys, ? for the list | | **Auto-reload** | Re-render the file at the same path and the player reloads it, keeping your place and speed | | **Resume** | Each file reopens where you left it | | **Library** | Every file shown in this container, one click away |@@ -39,7 +39,7 @@ Every reply starts with `OK:` or `ERROR:`, and every error carries a `Hint:` wit | `adom-video --ai-thread X play <file> [--title] [--chapters] [--captions] [--fps] [--name] [--surface wv\|hbw\|pup\|nb] [--no-open]` | Register a file and show it | | `adom-video --ai-thread X url <file>` | Register and print the player URL | | `adom-video --ai-thread X show` | Open the library page |-| `adom-video --ai-thread X ui toast\|seek\|play\|pause\|speed\|theater\|captions\|click\|reveal\|cursor\|reload ...` | Drive the open player over its live channel |+| `adom-video --ai-thread X ui toast\|seek\|play\|pause\|speed\|captions\|click\|reveal\|cursor\|reload ...` | Drive the open player over its live channel | | `adom-video ls` / `health` | The server, its version, open pages and the library | | `adom-video settings [key=value]` | The same settings as the Settings window, validated | | `adom-video --ai-thread X stop --reason "..." [--confirm]` | Stop the shared server (reason-gated; asks for confirmation while others use it) |
bin/adom-video+1−1
@@ -20,7 +20,7 @@ const HELP = `adom-video ${PKG.version}: play video and audio in a real, seekabl   adom-video --ai-thread "<name>" show [--surface S]  open the library page   adom-video --ai-thread "<name>" ui <directive> ...  drive the open player page:         toast "text" [--kind info|success|error]   seek <seconds>   play   pause   speed <x>-        click <selector>   reveal <selector>   cursor <selector>   theater   captions   reload+        click <selector>   reveal <selector>   cursor <selector>   captions   reload         eval "<js>"   (needs Developer mode: adom-video settings devMode=true)   adom-video ls                                      the server, its port and version, and the library   adom-video settings [key=value ...]                read or change settings (see the Settings window)
package.json+2−2
@@ -1,11 +1,11 @@ {   "slug": "adom-video-player",   "type": "app",-  "version": "0.4.1",+  "version": "0.4.2",   "title": "Adom Video Player",   "brief": "Show any video in a real player: seek, scrub, frame step, chapters. One command, no hand-rolled servers.",   "summary": "When an AI shows you a video or audio file, it plays here: a shared server that answers HTTP Range requests, so the timeline seeks, in the same YouTube-style player as the wiki's ::adom-video widget, with YouTube's shortcuts, chapters, captions, frame stepping and auto-reload when the file is re-rendered.",-  "description": "adom-video is the standard way for an AI to show a user a video or audio file in Hydrogen (or pup). It runs one small zero-dependency Node server per container that serves only the files you register, with full HTTP Range support (206 Partial Content, If-Range, HEAD, ETag, no-cache), which is what a browser needs to seek. A quick python -m http.server does not do this, so videos shown that way cannot be scrubbed. The player page is the wiki's ::adom-video widget (YouTube's look and every YouTube shortcut, a scrub timeline with frame preview and chapter segments, captions it draws itself, hold for 2x, frame stepping at the measured rate, speeds to 4x), plus theater mode, a chapter list, a library of everything shown in the container, per-file resume, and auto-reload with position kept when the file changes on disk. It follows the Adom app conventions: --ai-thread on every verb, one registry entry, reuse instead of respawn, a 24 hour idle reaper.",+  "description": "adom-video is the standard way for an AI to show a user a video or audio file in Hydrogen (or pup). It runs one small zero-dependency Node server per container that serves only the files you register, with full HTTP Range support (206 Partial Content, If-Range, HEAD, ETag, no-cache), which is what a browser needs to seek. A quick python -m http.server does not do this, so videos shown that way cannot be scrubbed. The player page is the wiki's ::adom-video widget (YouTube's look and every YouTube shortcut, a scrub timeline with frame preview and chapter segments, captions it draws itself, hold for 2x, frame stepping at the measured rate, speeds to 4x), plus a chapter list, a library of everything shown in the container, per-file resume, and auto-reload with position kept when the file changes on disk. It follows the Adom app conventions: --ai-thread on every verb, one registry entry, reuse instead of respawn, a 24 hour idle reaper.",   "category": "Developer Tools",   "tags": [     "video",
page.json+2−2
@@ -1,11 +1,11 @@ {   "slug": "adom-video-player",   "type": "app",-  "version": "0.4.1",+  "version": "0.4.2",   "title": "Adom Video Player",   "brief": "Show any video in a real player: seek, scrub, frame step, chapters. One command, no hand-rolled servers.",   "summary": "When an AI shows you a video or audio file, it plays here: a shared server that answers HTTP Range requests, so the timeline seeks, in the same YouTube-style player as the wiki's ::adom-video widget, with YouTube's shortcuts, chapters, captions, frame stepping and auto-reload when the file is re-rendered.",-  "description": "adom-video is the standard way for an AI to show a user a video or audio file in Hydrogen (or pup). It runs one small zero-dependency Node server per container that serves only the files you register, with full HTTP Range support (206 Partial Content, If-Range, HEAD, ETag, no-cache), which is what a browser needs to seek. A quick python -m http.server does not do this, so videos shown that way cannot be scrubbed. The player page is the wiki's ::adom-video widget (YouTube's look and every YouTube shortcut, a scrub timeline with frame preview and chapter segments, captions it draws itself, hold for 2x, frame stepping at the measured rate, speeds to 4x), plus theater mode, a chapter list, a library of everything shown in the container, per-file resume, and auto-reload with position kept when the file changes on disk. It follows the Adom app conventions: --ai-thread on every verb, one registry entry, reuse instead of respawn, a 24 hour idle reaper.",+  "description": "adom-video is the standard way for an AI to show a user a video or audio file in Hydrogen (or pup). It runs one small zero-dependency Node server per container that serves only the files you register, with full HTTP Range support (206 Partial Content, If-Range, HEAD, ETag, no-cache), which is what a browser needs to seek. A quick python -m http.server does not do this, so videos shown that way cannot be scrubbed. The player page is the wiki's ::adom-video widget (YouTube's look and every YouTube shortcut, a scrub timeline with frame preview and chapter segments, captions it draws itself, hold for 2x, frame stepping at the measured rate, speeds to 4x), plus a chapter list, a library of everything shown in the container, per-file resume, and auto-reload with position kept when the file changes on disk. It follows the Adom app conventions: --ai-thread on every verb, one registry entry, reuse instead of respawn, a 24 hour idle reaper.",   "category": "Developer Tools",   "tags": [     "video",
public/icons.js−1
@@ -16,7 +16,6 @@ window.ICONS = {   play: '<path d="M8 5.5v13l10.5-6.5z"/>',   pause: '<path d="M8.5 5.5v13M15.5 5.5v13"/>',   cc: '<rect x="3" y="5.5" width="18" height="13" rx="2"/><path d="M10.5 10.2a2.2 2.2 0 1 0 0 3.6M17 10.2a2.2 2.2 0 1 0 0 3.6"/>',-  theater: '<rect x="2.5" y="6.5" width="19" height="11" rx="1.5"/>',   pip: '<rect x="2.5" y="4.5" width="19" height="15" rx="2"/><rect x="12" y="11.5" width="7" height="5.5" rx="1"/>',   full: '<path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/>',   loop: '<path d="M17 3.5 20 6.5 17 9.5"/><path d="M4 11.5v-1a4 4 0 0 1 4-4h12"/><path d="M7 20.5 4 17.5 7 14.5"/><path d="M20 12.5v1a4 4 0 0 1-4 4H4"/>',
public/player.css−2
@@ -55,8 +55,6 @@ aside h2 { font: 600 12px var(--font-head); letter-spacing: .06em; text-transfor .list li .t { font: 500 12px var(--font-mono); color: var(--accent-bright); flex: none; } .list li.empty { color: var(--muted); cursor: default; font-size: 13px; } .list li.gone { opacity: .5; }-body.theater main { grid-template-columns: 1fr; }-body.theater aside { display: none; } .revealed { outline: 2px solid var(--accent); outline-offset: 2px; transition: outline .2s ease; } .lib-main { display: block; padding: 16px; height: auto; max-width: 980px; } .lib-intro { color: var(--muted); margin: 0 0 12px; }
public/player.html+1−2
@@ -48,11 +48,10 @@         <tr><td>+ / -</td><td>Caption size</td></tr>         <tr><td>O / W</td><td>Caption text / background opacity</td></tr>         <tr><td>F or double-click</td><td>Fullscreen</td></tr>-        <tr><td>T</td><td>Theater mode</td></tr>         <tr><td>I</td><td>Picture-in-picture</td></tr>         <tr><td>R</td><td>Loop</td></tr>         <tr><td>?</td><td>Shortcuts sheet on the video</td></tr>-        <tr><td>Esc</td><td>Close a menu, or leave theater mode</td></tr>+        <tr><td>Esc</td><td>Close a menu, or leave fullscreen</td></tr>   </table><div class="sc-foot">The same keys as YouTube. Press ? for this list on the video.</div></div> <dialog id="settings" aria-label="Settings">   <div class="dlg-head"><h3>Settings</h3><button class="icon-btn" id="settings-close" aria-label="Close settings" data-tooltip="Close settings (Esc)" data-icon="close"></button></div>
public/player.js+6−25
@@ -1,7 +1,7 @@ // adom-video-player page: the wiki's <adom-video> player (adom-video.js, vendored unchanged from // adom/widgets) in the app frame. The player owns playback, the timeline, captions, the // settings menu and YouTube's shortcuts; this page adds what only the app has: the library-// (Shift+N/P), theater mode (T), resume and settings on the server, reload on re-render,+// (Shift+N/P), resume and settings on the server, reload on re-render, // and the SSE channel that lets the AI drive it. All state lives on the server. (() => {   const { api, toast, hoverPopover, header, initSettings, connect, uiCommon } = window.UI;@@ -73,13 +73,10 @@     player = document.createElement('adom-video');     v = player.v;     const sr = player.shadowRoot;-    // T is theater mode here (in a wiki frame it opens the player large in a new tab), and the-    // pop-out button becomes YouTube's theater button-    player.popout = () => theater();-    const pop = sr.querySelector('[data-k=pop]');-    pop.dataset.k = 'theater'; pop.dataset.tip = 'Theater mode (t)';-    pop.innerHTML = '<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true"><path d="M19 6H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2zm0 10H5V8h14v8z"/></svg>';-    pop.addEventListener('click', () => theater());+    // in a wiki frame the pop-out button and T open the player large in a new tab; this app already+    // is the player, so the button goes, T does nothing, and popout is only the fullscreen fallback+    sr.querySelector('[data-k=pop]').hidden = true;+    player.popout = () => player._hud('Fullscreen is not available here');     // Shift+N / Shift+P and the media keys step through the library     player.sibling = (dir) => sibling(dir);     // ? shows the app's list (the player's own names the wiki's keys)@@ -98,7 +95,6 @@     v.addEventListener('pause', savePos);     v.addEventListener('seeked', () => { if (v.paused) savePos(); });     v.addEventListener('ratechange', () => { if (!player._hold) savePos(); });-    syncTheater();   }    // ---------- chapters, library ----------@@ -147,18 +143,6 @@   setInterval(() => { if (v && !v.paused && Math.abs(v.currentTime - lastSaved) > 2) savePos(); }, 3000);   addEventListener('pagehide', savePos); -  // ---------- theater -----------  function syncTheater() {-    const on = document.body.classList.contains('theater');-    const b = player && player.shadowRoot.querySelector('[data-k=theater]');-    if (b) { b.classList.toggle('on', on); b.setAttribute('aria-pressed', String(on)); b.dataset.tip = on ? 'Default view (t)' : 'Theater mode (t)'; }-  }-  function theater(on) {-    document.body.classList.toggle('theater', on ?? !document.body.classList.contains('theater'));-    syncTheater();-    player._hud(document.body.classList.contains('theater') ? 'Theater mode' : 'Default view');-  }-   // ---------- header ----------   $('copy').onclick = () => { navigator.clipboard?.writeText(location.href).then(() => toast('Link copied', 'success'), () => toast(location.href)); };   hoverPopover($('sc-btn'), $('sc-pop'));@@ -174,9 +158,7 @@     if (e.key === 'Escape' && document.fullscreenElement && !sr.querySelector('.menu.on, .help.on')) {       document.exitFullscreen().catch(() => {}); e.preventDefault(); e.stopPropagation(); return;     }-    if (e.key === 'Escape' && document.body.classList.contains('theater') && !sr.querySelector('.menu.on, .help.on')) {-      theater(false); e.preventDefault(); e.stopPropagation(); return;-    }+    if ((e.key === 't' || e.key === 'T') && !e.ctrlKey) { e.stopPropagation(); return; }  // the player's pop-out key     if (e.key === 'Enter' || e.key === 'Tab') return;     if (!sr.activeElement) player.p.focus({ preventScroll: true });   }, true);@@ -193,7 +175,6 @@       else if (d.type === 'play') v.play().catch(() => toast('The browser blocked autoplay; press Play', 'error'));       else if (d.type === 'pause') v.pause();       else if (d.type === 'speed') player.speed(Number(d.rate) || 1);-      else if (d.type === 'theater') theater();       else if (d.type === 'captions') player.captions();     },   });
server.js+1−1
@@ -186,7 +186,7 @@ function serveMedia(req, res, it) {   fs.createReadStream(it.path, { start, end }).pipe(res); } -const UI_DIRECTIVES = new Set(['toast', 'click', 'reveal', 'cursor', 'reload', 'seek', 'play', 'pause', 'speed', 'eval', 'theater', 'captions']);+const UI_DIRECTIVES = new Set(['toast', 'click', 'reveal', 'cursor', 'reload', 'seek', 'play', 'pause', 'speed', 'eval', 'captions']);  const server = http.createServer(async (req, res) => {   lastActivity = Date.now();
skills/adom-video-player/SKILL.md+2−2
@@ -39,7 +39,7 @@ It starts (or reuses) the one shared server, registers the file, and opens a Hyd | `--no-open` / `adom-video url <file>` | Only register and print the URL, for embedding |  Other verbs (every reply starts `OK:` or `ERROR:` with a `Hint:`; add `--json` for JSON):-- `adom-video --ai-thread X ui toast "..." | seek <s> | play | pause | speed <x> | theater | captions | click <sel> | cursor <sel> | reload`+- `adom-video --ai-thread X ui toast "..." | seek <s> | play | pause | speed <x> | captions | click <sel> | cursor <sel> | reload`   drives the OPEN player over its SSE channel (use toasts to narrate to the user while you drive it). - `adom-video ls` / `health`: server, version, open pages, library. `GET /state` and `GET /console` on the server for the full picture. - `adom-video settings [key=value]`: autoplay, rememberPosition, defaultSpeed, surface (wv, hbw, pup, nb), openInBackground, ttlHours, devMode.@@ -51,7 +51,7 @@ Other verbs (every reply starts `OK:` or `ERROR:` with a `Hint:`; add `--json` f  The same YouTube-style player as the wiki's `::adom-video` widget (vendored unchanged from adom/widgets): - A timeline that seeks anywhere (206 Partial Content, `If-Range`, `HEAD`, `ETag`): drag to scrub, hover for a frame preview, chapters as bar segments.-- YouTube's keyboard shortcuts, all of them: Space/K, J/L, arrows (seek and volume), M, `,` `.` frame (with a frame N of total pill), `<` `>` speed (0.25x to 4x), hold the video for 2x, 0 to 9, Home/End, Ctrl+arrows chapters, F or double-click fullscreen, T theater, I picture-in-picture, R loop, C captions, + - caption size, O W caption opacity, Shift+N/P next/previous video in the library, ? list. Every button's tooltip names its key.+- YouTube's keyboard shortcuts, all of them: Space/K, J/L, arrows (seek and volume), M, `,` `.` frame (with a frame N of total pill), `<` `>` speed (0.25x to 4x), hold the video for 2x, 0 to 9, Home/End, Ctrl+arrows chapters, F or double-click fullscreen, I picture-in-picture, R loop, C captions, + - caption size, O W caption opacity, Shift+N/P next/previous video in the library, ? list. Every button's tooltip names its key. - Speed, captions, loop and the shortcut list in the player's settings menu; a chapter list beside the player. - Re-render the file at the same path and the open player reloads it, keeping the position and speed. No stale cache. - Resumes where the user left off; a library of every file shown in this container (`/` on the server).