Tool walkthrough videos: collapsible player + volume-served /tv/ route

Twelve narrated walkthroughs, one per Suite tool, in Marty's cloned voice.
No screen recording involved: Playwright drives and records the real UI,
ElevenLabs narrates, ffmpeg muxes. Pipeline lives outside this repo at
../video-pipeline.

Placement (Marty's call): TOP of each tool page but COLLAPSED — a one-line
"Watch the walkthrough" bar that auto-opens on a member's first visit to that
specific tool and stays shut after. A video in a separate library never gets
watched; an embedded player pushes the tool below the fold on mobile for every
returning member. First-visit autoplay is muted, so it is never a surprise
noise, and every localStorage access is guarded because it throws outright in
private windows.

Served from the data volume via /tv/<slug>.mp4, NOT from public/. That
directory is already 199MB and another 33MB of MP4 would ride along on every
deploy forever; this way a video can also be re-cut and dropped in without a
rebuild. The route implements HTTP range requests — without them the scrub bar
renders but cannot seek, which looks broken in a way people rarely report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-28 14:14:21 -05:00
parent 6a7c5161ff
commit 1896b707f8
14 changed files with 154 additions and 1 deletions
+30 -1
View File
@@ -1062,6 +1062,35 @@ async function handleApi(req,res,pathname){
}catch(err){ return json(res,502,{error:String(err.message||err)}); }
}
// Tool walkthrough videos, served from the data volume rather than the image.
// 45MB of MP4 in the repo would ride along on every single deploy, and this
// way a video can be re-cut and dropped in without a rebuild.
//
// Range support is not optional here: without it the browser cannot seek, so
// the scrub bar looks present but does nothing.
if(req.method==='GET'&&/^\/tv\/[a-z0-9-]{1,60}\.mp4$/.test(pathname)){
const slug=pathname.slice(4).replace(/\.mp4$/,'');
const vfile=path.join(DATA_DIR,'tool-videos',slug+'.mp4');
let st=null;
try{ st=fs.statSync(vfile); }catch(err){ return json(res,404,{error:'No walkthrough for that tool yet.'}); }
const range=req.headers.range;
const base={'Content-Type':'video/mp4','Accept-Ranges':'bytes','Cache-Control':'public, max-age=3600'};
if(range){
const m=/bytes=(\d*)-(\d*)/.exec(range)||[];
const start=m[1]?parseInt(m[1],10):0;
const end=m[2]?parseInt(m[2],10):st.size-1;
if(start>=st.size||end>=st.size||start>end){
res.writeHead(416,securityHeaders(Object.assign({},base,{'Content-Range':'bytes */'+st.size})));
return res.end();
}
res.writeHead(206,securityHeaders(Object.assign({},base,{
'Content-Range':'bytes '+start+'-'+end+'/'+st.size,'Content-Length':(end-start+1)})));
return fs.createReadStream(vfile,{start,end}).pipe(res);
}
res.writeHead(200,securityHeaders(Object.assign({},base,{'Content-Length':st.size})));
return fs.createReadStream(vfile).pipe(res);
}
if(req.method==='POST'&&pathname==='/api/public/suite-traffic-stop'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup — try again.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error});
@@ -1366,7 +1395,7 @@ const server=http.createServer(async(req,res)=>{
const u=new URL(req.url,`http://${req.headers.host||'localhost'}`),pathname=decodeURIComponent(u.pathname);
// /p/<id> = member Page Builder pages: dynamic HTML, so it must reach the
// API handler rather than the static-file path (which 404s it).
if(pathname==='/health'||pathname.startsWith('/api/')||/^\/p\/\d{1,15}(\/[a-z0-9-]{1,24})?$/.test(pathname))return await handleApi(req,res,pathname);
if(pathname==='/health'||pathname.startsWith('/api/')||pathname.startsWith('/tv/')||/^\/p\/\d{1,15}(\/[a-z0-9-]{1,24})?$/.test(pathname))return await handleApi(req,res,pathname);
if(req.method!=='GET'&&req.method!=='HEAD')return send(res,405,'Method Not Allowed',{'Content-Type':'text/plain; charset=utf-8'});
// Members-area gate: the Circle Method lessons 2-10 + the e-gift cash-out
// walkthrough are the PRODUCT — their video FILES require a wallet-verified