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
16 changes: 15 additions & 1 deletion server/services/sharing/peerSyncAssets.js
Original file line number Diff line number Diff line change
Expand Up @@ -565,8 +565,22 @@ export async function buildFableLoomAssetManifest(loom) {
for (const entry of await Promise.all(imageNames.map((name) => hashImageForManifest(name)))) {
if (entry) dedup.set(`${entry.kind}:${entry.filename}`, entry);
}
// Typed playback assets (hold loops, transition exit clips, and an explicit
// entry clip) live under node.playbackAssets alongside the legacy single
// node.videoHistoryId — all reference on-disk <id>.mp4 files under
// PATHS.videos and must all be advertised, or a receiving peer never
// requests them and playback breaks post-sync (#6006).
const videoIds = [...new Set(
nodes.map((node) => (isStr(node?.videoHistoryId) ? node.videoHistoryId : null)).filter(Boolean),
nodes.flatMap((node) => {
const ids = [node?.videoHistoryId, node?.playbackAssets?.entryVideoHistoryId];
if (Array.isArray(node?.playbackAssets?.holdLoopVideoHistoryIds)) {
ids.push(...node.playbackAssets.holdLoopVideoHistoryIds);
}
if (node?.playbackAssets?.exitByTransition && typeof node.playbackAssets.exitByTransition === 'object') {
ids.push(...Object.values(node.playbackAssets.exitByTransition));
}
return ids.filter(isStr);
}),
)];
if (videoIds.length > 0) {
const entries = await Promise.all(videoIds.map((id) =>
Expand Down
45 changes: 45 additions & 0 deletions server/services/sharing/peerSyncAssets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,51 @@ describe('buildFableLoomAssetManifest — scene renders', () => {
});
expect(manifest).toHaveLength(2);
});

it('includes typed playback assets — entry, hold loops, and transition exits (#6006)', async () => {
const entryBytes = Buffer.from('entry-clip');
const holdBytes = Buffer.from('hold-clip');
const exitBytes = Buffer.from('exit-clip');
writeVideo('entry-1.mp4', entryBytes);
writeVideo('hold-1.mp4', holdBytes);
writeVideo('exit-1.mp4', exitBytes);

const manifest = await buildFableLoomAssetManifest({
episodes: [{
nodes: [{
id: 'node-1',
// No legacy node.videoHistoryId — only typed playbackAssets.
playbackAssets: {
entryVideoHistoryId: 'entry-1',
holdLoopVideoHistoryIds: ['hold-1'],
exitByTransition: { 'trans-1': 'exit-1' },
},
}],
}],
});

expect(manifest).toContainEqual({ filename: 'entry-1.mp4', kind: 'video', sha256: sha(entryBytes) });
expect(manifest).toContainEqual({ filename: 'hold-1.mp4', kind: 'video', sha256: sha(holdBytes) });
expect(manifest).toContainEqual({ filename: 'exit-1.mp4', kind: 'video', sha256: sha(exitBytes) });
expect(manifest).toHaveLength(3);
});

it('deduplicates a video id referenced by both node.videoHistoryId and playbackAssets', async () => {
const videoBytes = Buffer.from('shared-clip');
writeVideo('shared-1.mp4', videoBytes);

const manifest = await buildFableLoomAssetManifest({
episodes: [{
nodes: [{
id: 'node-1',
videoHistoryId: 'shared-1',
playbackAssets: { entryVideoHistoryId: 'shared-1', holdLoopVideoHistoryIds: ['shared-1'] },
}],
}],
});

expect(manifest).toEqual([{ filename: 'shared-1.mp4', kind: 'video', sha256: sha(videoBytes) }]);
});
});

describe('pullMissingAssetsFromPeer — unsafe and incomplete downloads (#5230)', () => {
Expand Down