diff --git a/packages/freecut-editor/package.json b/packages/freecut-editor/package.json index aa3d2f303..a702a3d65 100644 --- a/packages/freecut-editor/package.json +++ b/packages/freecut-editor/package.json @@ -1,6 +1,6 @@ { "name": "@quantfive/freecut-editor-surface", - "version": "0.3.0", + "version": "0.3.1", "description": "The host-backed FreeCut browser editor surface.", "license": "MIT", "repository": { diff --git a/src/features/preview/utils/decoder-prewarm.test.ts b/src/features/preview/utils/decoder-prewarm.test.ts index 4f4777f94..55acd50fe 100644 --- a/src/features/preview/utils/decoder-prewarm.test.ts +++ b/src/features/preview/utils/decoder-prewarm.test.ts @@ -22,6 +22,15 @@ import { clearObjectUrlRegistry, registerObjectUrl, } from '@/infrastructure/browser/object-url-registry' +import { blobUrlManager } from '@/infrastructure/browser/blob-url-manager' +import { updateMedia } from '@/infrastructure/storage' +import { setWorkspaceRoot } from '@/infrastructure/storage/workspace-fs/root' +import { clearKeyframeIndex, getKeyframeTimestamps } from '@/shared/utils/keyframe-index-registry' + +vi.mock('@/infrastructure/storage', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, updateMedia: vi.fn() } +}) type MockWorkerMessage = { type: string @@ -94,6 +103,7 @@ beforeEach(() => { mockBitmap = { close: vi.fn() } as unknown as ImageBitmap fetchMock = vi.fn() autoRespondPreseek = true + vi.mocked(updateMedia).mockClear() vi.stubGlobal('fetch', fetchMock) class WorkerStub extends MockWorker { @@ -109,6 +119,9 @@ beforeEach(() => { afterEach(() => { disposePrewarmWorker() clearObjectUrlRegistry() + blobUrlManager.releaseAll() + setWorkspaceRoot(null) + clearKeyframeIndex() vi.unstubAllGlobals() }) @@ -711,4 +724,48 @@ describe('decoder prewarm', () => { expect(mockBitmap.close).toHaveBeenCalledTimes(1) }) + + it('skips keyframe persistence when no workspace root is set (host-mounted surface)', () => { + // Host-mounted consumers (e.g. CodePress) never render , so + // requireWorkspaceRoot() inside updateMedia would throw on every decoded + // play/pause cycle. Persistence must be gated off; in-session registration + // still happens. + setWorkspaceRoot(null) + blobUrlManager.registerUrl('media-host-mounted', 'blob:kf-no-root') + warmDecoderPrewarmWorkerPool() + const worker = createdWorkers[0]! + + expect(() => + worker.onmessage?.({ + data: { + type: 'keyframes_extracted', + src: 'blob:kf-no-root', + keyframeTimestamps: [0, 1.5, 3], + }, + } as MessageEvent), + ).not.toThrow() + + expect(getKeyframeTimestamps('blob:kf-no-root')).toEqual([0, 1.5, 3]) + expect(updateMedia).not.toHaveBeenCalled() + }) + + it('persists extracted keyframes to storage when a workspace root is set', () => { + setWorkspaceRoot({ name: 'workspace' } as FileSystemDirectoryHandle) + blobUrlManager.registerUrl('media-persisted', 'blob:kf-with-root') + warmDecoderPrewarmWorkerPool() + const worker = createdWorkers[0]! + + worker.onmessage?.({ + data: { + type: 'keyframes_extracted', + src: 'blob:kf-with-root', + keyframeTimestamps: [0, 2], + }, + } as MessageEvent) + + expect(getKeyframeTimestamps('blob:kf-with-root')).toEqual([0, 2]) + expect(updateMedia).toHaveBeenCalledWith('media-persisted', { + keyframeTimestamps: [0, 2], + }) + }) }) diff --git a/src/features/preview/utils/decoder-prewarm.ts b/src/features/preview/utils/decoder-prewarm.ts index 7f6ffe757..e044b2c8b 100644 --- a/src/features/preview/utils/decoder-prewarm.ts +++ b/src/features/preview/utils/decoder-prewarm.ts @@ -22,6 +22,7 @@ import { } from '@/infrastructure/browser/object-url-registry' import { blobUrlManager } from '@/infrastructure/browser/blob-url-manager' import { updateMedia } from '@/infrastructure/storage' +import { getWorkspaceRoot } from '@/infrastructure/storage/workspace-fs/root' import { getKeyframeTimestamps, registerKeyframeIndex, @@ -229,9 +230,12 @@ function handleWorkerMessage(event: MessageEvent): void { // Register in main-thread registry for the export/edit overlay path. registerKeyframeIndex(msg.src, msg.keyframeTimestamps) keyframesSentForSrc.add(msg.src) - // Persist to IndexedDB so future sessions don't need re-extraction + // Persist to IndexedDB so future sessions don't need re-extraction. + // Host-mounted surfaces (no ) have no workspace root, so + // cross-session keyframe persistence is skipped there — the in-session + // registry above still serves the export/edit overlay path. const mediaId = blobUrlManager.getMediaIdByUrl(msg.src) - if (mediaId) { + if (mediaId && getWorkspaceRoot()) { void updateMedia(mediaId, { keyframeTimestamps: msg.keyframeTimestamps }) } return