Keep mobile controls visible and make recorded VOD remux seekable

This commit is contained in:
Technomancer702
2026-10-13 22:00:19 -07:00
parent a922c6dbb5
commit f72675030a
13 changed files with 172 additions and 45 deletions

View File

@ -217,3 +217,63 @@ test('legacy case-only accounts retain exact logins and reject ambiguous case va
const resolved = await fixture.request('/api/auth/login', post({ username: 'TWIN', password: 'first-password' }));
assert.equal(resolved.status, 200); assert.equal(resolved.body.user.username, 'Twin');
});
test('recorded TS movies and series use copy sessions, preserve totals and seek on a provider without ranges',
{ skip: process.env.NODECAST_BROWSER_TESTS === '1' ? false : 'Browser tests enabled in CI', timeout: 90000 }, async t => {
const { chromium }=require('playwright');
const fixture=await serverFixture(t);
const binary=spawnSync('ffmpeg',['-version'],{stdio:'ignore'}).status===0?'ffmpeg':require('ffmpeg-static');
const recorded=path.join(fixture.dir,'recorded.ts');
const generated=spawnSync(binary,['-hide_banner','-loglevel','error','-y',
'-f','lavfi','-i','color=c=red:s=160x90:r=10:d=12','-f','lavfi','-i','color=c=blue:s=160x90:r=10:d=12',
'-f','lavfi','-i','sine=frequency=1000:sample_rate=48000:duration=24',
'-filter_complex','[0:v][1:v]concat=n=2:v=1:a=0[v]','-map','[v]','-map','2:a',
'-c:v','libx264','-threads','1','-preset','ultrafast','-g','10','-c:a','aac','-ac','2','-f','mpegts',recorded],{encoding:'utf8',timeout:15000});
assert.equal(generated.status,0,generated.stderr);
const media=fs.readFileSync(recorded),provider=http.createServer((req,res)=>{
// This origin always ignores Range and sends the full recording progressively.
res.writeHead(200,{'Content-Type':'video/mp2t'});res.write(media);res.end();
});
await new Promise(resolve=>provider.listen(0,'127.0.0.1',resolve));
t.after(()=>new Promise(resolve=>{provider.close(resolve);provider.closeAllConnections();}));
const source=`http://127.0.0.1:${provider.address().port}/recorded.ts`;
const admin=await fixture.request('/api/auth/setup',post({username:'Owner',password:'owner-password'}));assert.equal(admin.status,201);
const cookie=admin.cookie.split(';')[0],headers={Cookie:cookie};
const browser=await chromium.launch({headless:true,executablePath:process.env.NODECAST_BROWSER_EXECUTABLE||undefined});t.after(()=>browser.close());
const context=await browser.newContext();const split=cookie.indexOf('=');
await context.addCookies([{name:cookie.slice(0,split),value:cookie.slice(split+1),url:fixture.base}]);
const page=await context.newPage(),requests=[],errors=[];
page.on('pageerror',error=>errors.push(error.message));
page.on('request',request=>{if(request.url().includes('/api/transcode/session'))requests.push(JSON.parse(request.postData()));});
await page.route('https://cdn.jsdelivr.net/npm/hls.js@1.5.7/dist/hls.min.js',route=>route.fulfill({contentType:'application/javascript',body:fs.readFileSync(require.resolve('hls.js/dist/hls.min.js'))}));
await page.route('https://fonts.**',route=>route.abort());
await page.route('**/api/probe?**',async route=>{const response=await route.fetch();const info=await response.json();await route.fulfill({response,json:{...info,duration:0}});});
await page.goto(fixture.base);await page.waitForFunction(()=>window.app?.pages?.watch);
for(const [type,settings]of [['movie',{autoTranscode:true,forceRemux:false}],['series',{autoTranscode:false,forceRemux:true}]]){
await fixture.request('/api/settings',{method:'PUT',headers,body:JSON.stringify(settings)});
await page.evaluate(async({source,type})=>{
const watch=window.app.pages.watch;window.app.navigateTo('watch');
watch.content={type};watch.resumeTime=0;await watch.loadVideo(source);
},{source,type});
await page.waitForFunction(()=>window.app.pages.watch.currentSessionId&&window.app.pages.watch.video.readyState>=2&&window.app.pages.watch.sourceDuration>20);
const original=await page.evaluate(()=>({session:window.app.pages.watch.currentSessionId,total:window.app.pages.watch.getPlaybackDuration()}));
assert.ok(original.total>23&&original.total<26,JSON.stringify(original));
assert.equal(requests.at(-1).videoMode,'copy');assert.equal(requests.at(-1).isLive,false);
const pixel=()=>page.evaluate(()=>{const canvas=document.createElement('canvas');canvas.width=canvas.height=1;canvas.getContext('2d').drawImage(window.app.pages.watch.video,0,0,1,1);return Array.from(canvas.getContext('2d').getImageData(0,0,1,1).data);});
let color=await pixel();assert.ok(color[0]>color[2]+100,JSON.stringify(color));
await page.evaluate(()=>window.app.pages.watch.seekToContentTime(16));
await page.waitForFunction(old=>window.app.pages.watch.currentSessionId!==old&&window.app.pages.watch.video.readyState>=2,original.session);
color=await pixel();assert.ok(color[2]>color[0]+100,JSON.stringify(color));
assert.equal(requests.at(-1).seekOffset,16);
assert.equal(await page.evaluate(()=>window.app.pages.watch.getPlaybackDuration()),original.total);
const forward=await page.evaluate(()=>window.app.pages.watch.currentSessionId);
assert.equal((await fixture.request(`/api/transcode/${original.session}/stream.m3u8`,{headers})).status,404);
await page.evaluate(()=>window.app.pages.watch.seekToContentTime(4));
await page.waitForFunction(old=>window.app.pages.watch.currentSessionId!==old&&window.app.pages.watch.video.readyState>=2,forward);
color=await pixel();assert.ok(color[0]>color[2]+100,JSON.stringify(color));
assert.equal(requests.at(-1).seekOffset,4);
assert.equal(await page.evaluate(()=>window.app.pages.watch.getPlaybackDuration()),original.total);
await page.evaluate(()=>window.app.pages.watch.stop());
}
assert.deepEqual(errors,[]);await page.close();
});