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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user