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
2 changes: 1 addition & 1 deletion packages/freecut-editor/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
57 changes: 57 additions & 0 deletions src/features/preview/utils/decoder-prewarm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof import('@/infrastructure/storage')>()
return { ...actual, updateMedia: vi.fn() }
})

type MockWorkerMessage = {
type: string
Expand Down Expand Up @@ -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 {
Expand All @@ -109,6 +119,9 @@ beforeEach(() => {
afterEach(() => {
disposePrewarmWorker()
clearObjectUrlRegistry()
blobUrlManager.releaseAll()
setWorkspaceRoot(null)
clearKeyframeIndex()
vi.unstubAllGlobals()
})

Expand Down Expand Up @@ -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 <WorkspaceGate>, 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],
})
})
})
8 changes: 6 additions & 2 deletions src/features/preview/utils/decoder-prewarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 <WorkspaceGate>) 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
Expand Down
Loading