WIP: Phase 3B playback engine handoff

This commit is contained in:
Media
2026-08-16 21:50:19 -05:00
parent e2ecdc97e2
commit 68273d457f
16 changed files with 3506 additions and 244 deletions
+35 -17
View File
@@ -4,6 +4,7 @@ const crypto = require("node:crypto");
const { openDatabase } = require("../src/database");
const { runMigrations } = require("../src/database/migrate");
const { createAssetService } = require("../src/assets/service");
const { createPlaybackService } = require("../src/playback");
const {
MAX_DURATION_MS,
MIN_DURATION_MS,
@@ -19,13 +20,22 @@ function createHarness() {
let idCounter = 0;
let assetCounter = 0;
const changes = [];
let playbackService;
const service = createPlaylistService({
database,
now: () => clock,
createId: () =>
`00000000-0000-4000-8000-${String(++idCounter).padStart(12, "0")}`,
onChanged: (change) => changes.push(change),
onBeforeChange: () => {
playbackService?.reconcile("before_domain_change");
},
onChanged: (change) => {
changes.push(change);
playbackService?.domainChanged(change);
},
});
playbackService = createPlaybackService({ database, now: () => clock });
playbackService.initialize();
function setNow(value) {
clock = value;
@@ -71,7 +81,15 @@ function createHarness() {
.get();
}
return { changes, database, playback, seedAsset, service, setNow };
return {
changes,
database,
playback,
playbackService,
seedAsset,
service,
setNow,
};
}
function expectError(action, code, statusCode) {
@@ -479,7 +497,7 @@ test("activation queues at a live boundary, is idempotent, and mode off is only
durationOverrideMs: 4000,
});
const initial = harness.service.activate(oldPlaylist.id);
const initial = harness.playbackService.activatePlaylist(oldPlaylist.id);
assert.equal(initial.activation, "active");
assert.equal(initial.playback.currentEntryId, oldEntry.id);
assert.equal(initial.playback.slideStartedAt, BASE_TIME);
@@ -489,7 +507,7 @@ test("activation queues at a live boundary, is idempotent, and mode off is only
.prepare("UPDATE playback_state SET mode = 'off' WHERE output_id = 'global'")
.run();
harness.setNow(BASE_TIME + 1000);
const pending = harness.service.activate(newPlaylist.id);
const pending = harness.playbackService.activatePlaylist(newPlaylist.id);
assert.equal(pending.activation, "pending");
assert.equal(pending.playback.mode, "off");
assert.equal(pending.playback.activePlaylistId, oldPlaylist.id);
@@ -498,18 +516,18 @@ test("activation queues at a live boundary, is idempotent, and mode off is only
const pendingRevision = pending.playback.revision;
const requestedAt = pending.playback.pendingPlaylistRequestedAt;
const repeated = harness.service.activate(newPlaylist.id);
const repeated = harness.playbackService.activatePlaylist(newPlaylist.id);
assert.equal(repeated.activation, "pending");
assert.equal(repeated.playback.revision, pendingRevision);
assert.equal(repeated.playback.pendingPlaylistRequestedAt, requestedAt);
const cancel = harness.service.activate(oldPlaylist.id);
const cancel = harness.playbackService.activatePlaylist(oldPlaylist.id);
assert.equal(cancel.activation, "already_active");
assert.equal(cancel.playback.pendingPlaylistId, null);
assert.equal(cancel.playback.currentEntryId, oldEntry.id);
harness.setNow(BASE_TIME + 7000);
const immediate = harness.service.activate(newPlaylist.id);
const immediate = harness.playbackService.activatePlaylist(newPlaylist.id);
assert.equal(immediate.activation, "active");
assert.equal(immediate.playback.activePlaylistId, newPlaylist.id);
assert.equal(immediate.playback.pendingPlaylistId, null);
@@ -534,7 +552,7 @@ test("paused normal playback queues and can replace a pending activation", (t) =
harness.service.addEntry(secondTarget.id, {
assetId: harness.seedAsset({ id: "second-target", name: "Second target" }),
});
harness.service.activate(current.id);
harness.playbackService.activatePlaylist(current.id);
harness.database
.prepare(
`UPDATE playback_state
@@ -545,14 +563,14 @@ test("paused normal playback queues and can replace a pending activation", (t) =
.run();
harness.setNow(BASE_TIME + 1000);
const first = harness.service.activate(firstTarget.id);
const first = harness.playbackService.activatePlaylist(firstTarget.id);
assert.equal(first.activation, "pending");
assert.equal(first.playback.activePlaylistId, current.id);
assert.equal(first.playback.pendingPlaylistId, firstTarget.id);
assert.equal(first.playback.pausedRemainingMs, 5000);
harness.setNow(BASE_TIME + 2000);
const replacement = harness.service.activate(secondTarget.id);
const replacement = harness.playbackService.activatePlaylist(secondTarget.id);
assert.equal(replacement.activation, "pending");
assert.equal(replacement.playback.activePlaylistId, current.id);
assert.equal(replacement.playback.pendingPlaylistId, secondTarget.id);
@@ -579,7 +597,7 @@ test("static and paused activation prepare the first playable normal target with
durationOverrideMs: 9000,
});
assert.equal(skipped.position, 0);
harness.service.activate(oldPlaylist.id);
harness.playbackService.activatePlaylist(oldPlaylist.id);
harness.database
.prepare(
`UPDATE playback_state
@@ -590,7 +608,7 @@ test("static and paused activation prepare the first playable normal target with
)
.run(staticAssetId);
const result = harness.service.activate(target.id);
const result = harness.playbackService.activatePlaylist(target.id);
assert.equal(result.activation, "active");
assert.equal(result.playback.activePlaylistId, target.id);
assert.equal(result.playback.staticAssetId, staticAssetId);
@@ -611,7 +629,7 @@ test("empty activation is immediate and current-entry deletion coordinates playb
assetId: harness.seedAsset({ id: `asset-${id}`, name: id }),
}),
);
harness.service.activate(populated.id);
harness.playbackService.activatePlaylist(populated.id);
const futurePlaybackTimestamp = BASE_TIME + 100_000;
harness.database
.prepare(
@@ -624,10 +642,10 @@ test("empty activation is immediate and current-entry deletion coordinates playb
harness.service.removeEntry(populated.id, entries[0].id);
const afterDelete = harness.playback();
assert.equal(afterDelete.current_entry_id, null);
assert.equal(afterDelete.current_entry_id, entries[1].id);
assert.equal(afterDelete.slide_started_at, null);
assert.equal(afterDelete.next_slide_at, null);
assert.equal(afterDelete.paused_remaining_ms, null);
assert.equal(afterDelete.paused_remaining_ms, 7000);
assert.equal(afterDelete.active_playlist_id, populated.id);
assert.equal(afterDelete.mode, "off");
assert.equal(afterDelete.static_asset_id, "asset-second");
@@ -646,7 +664,7 @@ test("empty activation is immediate and current-entry deletion coordinates playb
WHERE output_id = 'global'`,
)
.run();
const result = harness.service.activate(empty.id);
const result = harness.playbackService.activatePlaylist(empty.id);
assert.equal(result.activation, "active");
assert.equal(result.playback.activePlaylistId, empty.id);
assert.equal(result.playback.currentEntryId, null);
@@ -665,7 +683,7 @@ test("inactive playlist edits do not mutate authoritative playback state", (t) =
const draftEntry = harness.service.addEntry(inactive.id, {
assetId: harness.seedAsset({ id: "draft", name: "Draft asset" }),
});
harness.service.activate(active.id);
harness.playbackService.activatePlaylist(active.id);
const before = harness.playback();
harness.service.update(inactive.id, { name: "Draft edited" });