Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/media-showcase.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,15 @@ the unpublished candidate cohort is retired before any held body reaches HLS;
the empty attached player joins the observed epoch. Pause and cancellation retire
local preparation. A published epoch is never moved backwards. Unsupported
metadata and browsers without a frame witness retain the ordinary startup path.
The latest M4 checks below qualify this revision for two fresh browser contexts
An adopted epoch can first appear in an unready publication notification and
then become ready after its ACK. Recorded playback compares that ready epoch
with the recording's bound epoch, so the intervening notification cannot hide
the required handoff. A losing unpresented candidate retires its view and peer
scope on the same empty player and original pending Play; a presented pipeline
keeps its existing replacement path. Same-epoch ACKs do not reload the source.
Controller fixtures cover both notification stages and stale callbacks. They
do not establish a cause for every cold-start failure or qualify fresh playback.
The M4 checks below qualify their recorded source revision for two fresh browser contexts
on one direct route, including an actual recording wrap. Physical phones,
cross-network traversal and public TURN remain separate acceptance gates.

Expand Down
2 changes: 1 addition & 1 deletion site/live-media.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,6 @@
</main>
<!-- SHOWCASE_END -->
<footer class="site-footer"><div class="page-wrap"><div class="footer-inner"><a class="brand" href="/">Forge<span>Sworn</span></a><div class="footer-links"><a href="https://github.com/forgesworn">GitHub</a><a href="/live-media-benchmarks.html">Measured results</a><a href="https://github.com/sponsors/TheCryptoDonkey">Support the work ↗</a></div></div><p class="footer-note">Open-source media software, yours to run. The supplied mix is licensed for this demonstration; its media rights are separate from the MIT code. <a href="/media/dejavu-preview/NOTICE.txt">Media permission</a> · <a href="/llms.txt">For developers and LLMs</a></p></div></footer>
<script type="module" src="/media-lab.mjs?v=20261026-startup-diagnostics"></script>
<script type="module" src="/media-lab.mjs?v=20261026-publishing-epoch"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion site/media-lab.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,6 @@
</main>
<!-- SHOWCASE_END -->
<footer class="site-footer"><div class="page-wrap"><div class="footer-inner"><a class="brand" href="/">Forge<span>Sworn</span></a><div class="footer-links"><a href="https://github.com/forgesworn">GitHub</a><a href="/live-media-benchmarks.html">Measured results</a><a href="https://github.com/sponsors/TheCryptoDonkey">Support the work ↗</a></div></div><p class="footer-note">Open-source media software, yours to run. The supplied mix is licensed for this demonstration; its media rights are separate from the MIT code. <a href="/media/dejavu-preview/NOTICE.txt">Media permission</a> · <a href="/llms.txt">For developers and LLMs</a></p></div></footer>
<script type="module" src="/media-lab.mjs?v=20261026-startup-diagnostics"></script>
<script type="module" src="/media-lab.mjs?v=20261026-publishing-epoch"></script>
</body>
</html>
7 changes: 5 additions & 2 deletions site/media-lab.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -447,9 +447,12 @@ async function initialise() {
if (request !== publicRoomRequest || pageAbort.signal.aborted) return;
const s = session?.quality === quality ? session : null;
if (s && !s.finishing) {
const previous = s.listenerState?.epochId;
// Publishing can name a new epoch before it is acknowledged. A
// recording must compare the ready epoch with its actual binding,
// rather than that intervening unready notification.
const boundEpoch = s.recordedEpochId ?? s.listenerState?.epochId;
s.listenerState = state;
if (previous && state.ready && state.epochId !== previous) {
if (boundEpoch && state.ready && state.epochId !== boundEpoch) {
s.followLive = true;
if (s.listenerPreparation && s.recordedView?.timelineOriginMs === null && !s.framePipeline?.seenFrame) {
// Another starter won while this empty pipeline buffered. No
Expand Down
2 changes: 1 addition & 1 deletion site/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,6 @@ <h2 class="reveal">Pick a block and start</h2>
});
});
</script>
<script type="module" src="/media-lab.mjs?v=20261026-startup-diagnostics"></script>
<script type="module" src="/media-lab.mjs?v=20261026-publishing-epoch"></script>
</body>
</html>
110 changes: 89 additions & 21 deletions test/media-showcase-lifecycle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,36 +29,60 @@ const fixtures = {
'/media-listener-session.mjs': `export function createListenerSession({onChange,room,duration}) {
fixture.roomConfig={room,duration};
const position=()=>fixture.recorded?((Date.now()-fixture.startedAt)/1000)%fixture.sourceDuration:50;
let joined=false,prepared=false,token=null,generation=0;
const snapshot=()=>({ready:joined,prepared,observed:true,connectedRelays:1,joined,
listeners:joined?1:0,epochId:fixture.coldRoom&&!prepared&&!joined?null:fixture.epochId,
position:prepared||fixture.coldRoom&&!joined?0:position(),startedAt:prepared?null:fixture.startedAt});
let joined=false,prepared=false,publishing=false,foreign=false,token=null,generation=0,cancelCommit=null;
const snapshot=()=>({ready:joined,prepared,observed:true,connectedRelays:1,joined:joined||publishing,
status:prepared?'prepared':publishing?'publishing':joined?'active':'observing',
listeners:(joined?1:0)+(foreign?1:0),epochId:fixture.coldRoom&&!prepared&&!joined&&!publishing&&!foreign?null:fixture.epochId,
position:prepared||fixture.coldRoom&&!joined&&!publishing&&!foreign?0:position(),startedAt:prepared?null:fixture.startedAt});
const notify=()=>onChange(snapshot()); fixture.roomUpdate=notify;
fixture.listenerSnapshot=snapshot;
fixture.replaceEpoch=()=>{fixture.epochId='2'.repeat(64);fixture.startedAt=Date.now()-10_000;notify()};
fixture.adoptPublishingEpoch=()=>{if(!publishing)throw new Error('Expected publishing');
foreign=true;fixture.epochId='2'.repeat(64);fixture.startedAt=Date.now()-10_000;notify()};
const roomApi={snapshot,position,observe:notify,join:async()=>{
const request=++generation; fixture.trace.push({kind:'join'});
if(fixture.holdJoin) await new Promise(resolve=>fixture.joinWaiters.push(resolve));
if(request!==generation) throw new DOMException('Join cancelled','AbortError'); joined=true; notify();
},leave:()=>{generation++;joined=false;prepared=false;token=null;notify()},stop(){generation++}};
if(request!==generation) throw new DOMException('Join cancelled','AbortError');
publishing=true;notify();await Promise.resolve();
if(request!==generation)throw new DOMException('Join cancelled','AbortError');publishing=false;joined=true;notify();
},leave:()=>{generation++;cancelCommit?.();joined=false;prepared=false;publishing=false;token=null;notify()},
stop(){generation++;cancelCommit?.();joined=false;prepared=false;publishing=false;token=null}};
if(fixture.coldRoom){
roomApi.prepareJoin=async()=>{
const request=++generation;fixture.trace.push({kind:'join'});
if(fixture.holdJoin)await new Promise(resolve=>fixture.joinWaiters.push(resolve));
if(request!==generation)throw new DOMException('Prepare cancelled','AbortError');
if(joined)return {...snapshot(),token:null};
if(foreign){await roomApi.join();return {...snapshot(),prepared:false,token:null};}
fixture.preparations=(fixture.preparations||0)+1;
if(fixture.preparations>1)fixture.epochId='3'.repeat(64);
prepared=true;token=Object.freeze({});notify();return {...snapshot(),token};
};
roomApi.commitPreparedStart=async(value,{signal}={})=>{
const request=generation;fixture.trace.push({kind:'commit'});
if(value!==token||!prepared||signal?.aborted)throw new DOMException('Invalid preparation','AbortError');
if(fixture.holdCommit)await new Promise(resolve=>fixture.commitWaiters.push(resolve));
if(request!==generation||value!==token||signal?.aborted)throw new DOMException('Commit cancelled','AbortError');
const adopted=fixture.adoptOnCommit===true;
if(adopted)fixture.epochId='2'.repeat(64);
fixture.startedAt=Date.now()-(adopted?10_000:0);prepared=false;token=null;joined=true;notify();
return {...snapshot(),adopted};
const candidate=fixture.epochId;
if(fixture.adoptOnCommit){foreign=true;fixture.epochId='2'.repeat(64);}
fixture.startedAt=Date.now()-(foreign?10_000:0);prepared=false;token=null;publishing=true;
const result=new Promise((resolve,reject)=>{
let settled=false;
const aborted=()=>roomApi.leave();
const cleanup=()=>{signal?.removeEventListener('abort',aborted);if(cancelCommit===cancel)cancelCommit=null};
const cancel=()=>{if(settled)return;settled=true;cleanup();reject(new DOMException('Commit cancelled','AbortError'))};
const acknowledge=()=>{
if(settled)return;
if(request!==generation||signal?.aborted){cancel();return;}
// The real actor removes its operation abort listener BEFORE it
// notifies active: that callback may retire the losing SDK.
settled=true;cleanup();publishing=false;joined=true;notify();
resolve({...snapshot(),adopted:fixture.epochId!==candidate});
};
cancelCommit=cancel;signal?.addEventListener('abort',aborted,{once:true});
fixture.commitWaiters.push(acknowledge);
notify();
if(!fixture.holdCommit)queueMicrotask(acknowledge);
});
return result;
};
}
return roomApi;
Expand Down Expand Up @@ -572,6 +596,11 @@ try {
.then(()=>fixture.commitOutcome='done',error=>fixture.commitOutcome=error.name);
});
await cold.page.waitForFunction(()=>fixture.trace.some(x=>x.kind==='commit'));
assert.deepEqual(await cold.page.evaluate(()=>{const s=fixture.listenerSnapshot();return{
prepared:s.prepared,joined:s.joined,ready:s.ready,status:s.status,
sources:fixture.trace.filter(x=>x.kind==='source').length,peers:fixture.peers.length}}),
{prepared:false,joined:true,ready:false,status:'publishing',sources:1,peers:1},
'publication is joined but unready while its real ACK is pending');
assert.equal(await cold.page.evaluate(()=>fixture.views[0].mediaTimeAt(Date.now())),0,'pending ACK cannot start the local clock');
await cold.page.evaluate(()=>fixture.releaseCommit());
await cold.page.waitForFunction(()=>fixture.commitOutcome==='done');
Expand All @@ -584,20 +613,34 @@ try {
});
assert(anchored.target>=0&&anchored.target<0.75,'fresh playback starts against the newly acknowledged anchor');
assert.deepEqual([anchored.players,anchored.attaches,anchored.plays],[1,1,1]);assert.equal(anchored.epoch,'1'.repeat(64));
await cold.page.evaluate(()=>fixture.roomUpdate());
assert.deepEqual(await cold.page.evaluate(()=>[fixture.players.length,fixture.peers.length,fixture.trace.filter(x=>x.kind==='source').length]),[1,1,1],
'same-epoch publication, ACK and repeated ready observations do not churn the recording scope');
await close(cold);

for(const adopt of [false,true]){
for(const action of ['pause','adopt-at-commit','adopt-while-publishing','abort','pagehide']){
const adopt=action.startsWith('adopt');
const pending=await open({recorded:true,coldRoom:true,holdCommit:true});
await pending.page.locator('#public-play').click();await pending.page.evaluate(()=>fixture.releaseRoom());
await pending.page.waitForFunction(()=>fixture.peers.length===1&&typeof fixture.peers[0].options.startupBufferCommit==='function');
await pending.page.evaluate(()=>{
fixture.peers[0].options.startupBufferCommit({signal:new AbortController().signal})
await pending.page.waitForFunction(()=>fixture.peers.length===1&&fixture.trace.some(x=>x.kind==='startLoad'));
await pending.page.evaluate(action=>{
fixture.adoptOnCommit=action==='adopt-at-commit';fixture.oldMetadata=fixture.metadataHandlers.at(-1);
fixture.commitController=new AbortController();
fixture.peers[0].options.startupBufferCommit({signal:fixture.commitController.signal})
.then(()=>fixture.commitOutcome='done',error=>fixture.commitOutcome=error.name);
});
await pending.page.waitForFunction(()=>fixture.trace.some(x=>x.kind==='commit'));
await pending.page.evaluate(adopt=>{if(adopt)fixture.adoptOnCommit=true;else document.getElementById('lab-video').pause();fixture.releaseCommit()},adopt);
},action);
await pending.page.waitForFunction(()=>fixture.listenerSnapshot().status==='publishing');
await pending.page.evaluate(action=>{
if(action==='adopt-while-publishing')fixture.adoptPublishingEpoch();
else if(action==='pause')document.getElementById('lab-video').pause();
else if(action==='abort')fixture.commitController.abort();
else if(action==='pagehide')window.dispatchEvent(new PageTransitionEvent('pagehide'));
fixture.releaseCommit();
},action);
await pending.page.waitForFunction(()=>fixture.commitOutcome==='AbortError');
if(adopt){
assert.equal(await pending.page.evaluate(()=>fixture.peers[0].stopped),true,
'ACK of a changed ready epoch retires the loaded unpresented candidate after its unready notification');
await pending.page.waitForFunction(()=>fixture.peers.length===2&&fixture.trace.filter(x=>x.kind==='source').length===2);
const converged=await pending.page.evaluate(()=>({players:fixture.players.length,attaches:fixture.trace.filter(x=>x.kind==='attach').length,
plays:fixture.trace.filter(x=>x.kind==='play').length,oldStopped:fixture.peers[0].stopped,
Expand All @@ -606,8 +649,26 @@ try {
assert.deepEqual([converged.players,converged.attaches,converged.plays],[1,1,1],'a losing unpresented candidate reuses the trusted pipeline');
assert.equal(converged.oldStopped,true);assert.equal(converged.epoch,'2'.repeat(64));assert(converged.target>0);
assert.equal(converged.metadataHandlers,2,'new epoch arms its own initial metadata alignment');
}else{
const retired=await pending.page.evaluate(async()=>{
const old=fixture.peers[0],before=document.getElementById('public-peer-bytes').dataset.bytes;
const seeks=fixture.trace.filter(x=>x.kind==='seek').length;
fixture.oldMetadata();old.options.onStats({peerBytes:999999});
const result=await old.options.startupBufferCommit({signal:new AbortController().signal}).then(()=>false,error=>error.name==='AbortError');
fixture.roomUpdate();
return{target:old.options.startupBufferTarget(),rejected:result,bytes:document.getElementById('public-peer-bytes').dataset.bytes,
unchanged:before,seeks:fixture.trace.filter(x=>x.kind==='seek').length-seeks};
});
assert.equal(retired.target,null);assert.equal(retired.rejected,true);assert.equal(retired.bytes,retired.unchanged);
assert.equal(retired.seeks,0,'old metadata and cohort callbacks cannot reach the fresh scope');
assert.deepEqual(await pending.page.evaluate(()=>[fixture.players.length,fixture.peers.length,fixture.trace.filter(x=>x.kind==='source').length]),[1,2,2],
'a repeated same ready epoch does not create another scope');
}else if(action==='pause'){
assert.equal(await pending.page.evaluate(()=>fixture.trace.filter(x=>x.kind==='source').length),1,'late cancelled activation cannot reload or resume');
const pausedMetadata=await pending.page.evaluate(()=>{
const seeks=fixture.trace.filter(x=>x.kind==='seek').length;fixture.oldMetadata();
return{seeks:fixture.trace.filter(x=>x.kind==='seek').length-seeks,target:fixture.peers[0].options.startupBufferTarget()};
});
assert.deepEqual(pausedMetadata,{seeks:0,target:null},'a paused publication cannot release its old cohort or metadata');
await pending.page.evaluate(()=>{void document.getElementById('lab-video').play()});
await pending.page.waitForFunction(()=>fixture.peers.length===2&&fixture.trace.filter(x=>x.kind==='source').length===2);
await pending.page.evaluate(async()=>{
Expand All @@ -619,10 +680,17 @@ try {
oldStopped:fixture.peers[0].stopped}));
assert.deepEqual([resumed.players,resumed.attaches,resumed.plays],[1,1,2],'explicit resume prepares afresh on the same empty player');
assert.equal(resumed.oldStopped,true);assert.equal(resumed.epoch,'3'.repeat(64));
}else{
const late=await pending.page.evaluate(()=>{
const seeks=fixture.trace.filter(x=>x.kind==='seek').length;fixture.oldMetadata();
return{sources:fixture.trace.filter(x=>x.kind==='source').length,plays:fixture.trace.filter(x=>x.kind==='play').length,
seeks:fixture.trace.filter(x=>x.kind==='seek').length-seeks,target:fixture.peers[0].options.startupBufferTarget()};
});
assert.deepEqual(late,{sources:1,plays:1,seeks:0,target:null},'aborted/departed publication cannot activate old bytes or metadata');
}
await close(pending);
}
checks.push('first room buffers at zero before activation; ACK anchors one trusted pipeline; pause and competing epochs retire candidate authority');
checks.push('first room buffers at zero; two-phase publishing/ACK retains one trusted pipeline or hands off the bound epoch; pause, abort and departure fence retired authority');

const preFrameResume=await open({recorded:true});
await preFrameResume.page.locator('#public-play').click();await preFrameResume.page.evaluate(()=>fixture.releaseRoom());
Expand Down
Loading