diff --git a/app/packages/core/src/plugins/SchemaIO/components/FrameLoaderView.tsx b/app/packages/core/src/plugins/SchemaIO/components/FrameLoaderView.tsx new file mode 100644 index 0000000000..ff41d1274e --- /dev/null +++ b/app/packages/core/src/plugins/SchemaIO/components/FrameLoaderView.tsx @@ -0,0 +1,95 @@ +import React, { useEffect, useRef, useState } from "react"; +import { ObjectSchemaType, ViewPropsType } from "../utils/types"; +import { + DEFAULT_FRAME_NUMBER, + GLOBAL_TIMELINE_ID, +} from "@fiftyone/playback/src/lib/constants"; +import { BufferManager, BufferRange } from "@fiftyone/utilities"; +import { usePanelEvent } from "@fiftyone/operators"; +import { usePanelId, useSetPanelStateById } from "@fiftyone/spaces"; +import { useTimeline } from "@fiftyone/playback/src/lib/use-timeline"; +import _ from "lodash"; + +export default function FrameLoaderView(props: ViewPropsType) { + const { schema, path, data } = props; + const { view = {} } = schema; + const { on_load_range, timeline_id, target } = view; + const panelId = usePanelId(); + const triggerEvent = usePanelEvent(); + const setPanelState = useSetPanelStateById(true); + const localIdRef = React.useRef(); + const bufm = useRef(new BufferManager()); + + useEffect(() => { + localIdRef.current = Math.random().toString(36).substring(7); + if (data?.frames) + window.dispatchEvent( + new CustomEvent(`frames-loaded`, { + detail: { localId: localIdRef.current }, + }) + ); + }, [data?.signature]); + + const loadRange = React.useCallback( + async (range: BufferRange) => { + if (on_load_range) { + const unp = bufm.current.getUnprocessedBufferRange(range); + const isProcessed = unp === null; + + if (!isProcessed) { + await triggerEvent(panelId, { + params: { range: unp }, + operator: on_load_range, + }); + } + + return new Promise((resolve) => { + window.addEventListener(`frames-loaded`, (e) => { + if ( + e instanceof CustomEvent && + e.detail.localId === localIdRef.current + ) { + bufm.current.addNewRange(range); + resolve(); + } + }); + }); + } + }, + [triggerEvent, on_load_range, localIdRef.current] + ); + + const [currentFrame, setCurrentFrame] = useState(DEFAULT_FRAME_NUMBER); + + const myRenderFrame = React.useCallback( + (frameNumber: number) => { + setPanelState(panelId, (current) => { + const currentData = current.data ? _.cloneDeep(current.data) : {}; // Clone the object + const currentFrameData = _.get(currentData, path, { frames: [] }) + .frames[frameNumber]; + let updatedData = { ...currentData }; + _.set(updatedData, target, currentFrameData); // Use lodash set to update safely + return { ...current, data: updatedData }; + }); + setCurrentFrame(frameNumber); + }, + [data, setPanelState, panelId, target] + ); + + const { isTimelineInitialized, subscribe } = useTimeline(); + const [subscribed, setSubscribed] = useState(false); + + React.useEffect(() => { + if (subscribed) return; + if (isTimelineInitialized) { + subscribe({ + id: timeline_id || GLOBAL_TIMELINE_ID, + loadRange, + renderFrame: myRenderFrame, + }); + setSubscribed(true); + } + }, [isTimelineInitialized, loadRange, myRenderFrame, subscribe]); + + return null; +} diff --git a/app/packages/core/src/plugins/SchemaIO/components/index.ts b/app/packages/core/src/plugins/SchemaIO/components/index.ts index d837b31dfc..bb0fca6f6e 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/index.ts +++ b/app/packages/core/src/plugins/SchemaIO/components/index.ts @@ -48,3 +48,4 @@ export { default as TagsView } from "./TagsView"; export { default as TextFieldView } from "./TextFieldView"; export { default as TupleView } from "./TupleView"; export { default as UnsupportedView } from "./UnsupportedView"; +export { default as FrameLoaderView } from "./FrameLoaderView"; diff --git a/app/packages/embeddings/src/index.ts b/app/packages/embeddings/src/index.ts index 29c0006e33..1e8524ec9a 100644 --- a/app/packages/embeddings/src/index.ts +++ b/app/packages/embeddings/src/index.ts @@ -16,5 +16,3 @@ registerComponent({ priority: BUILT_IN_PANEL_PRIORITY_CONST, }, }); - -// registerOperator(new OpenEmbeddingsPanel()); diff --git a/app/packages/playback/package.json b/app/packages/playback/package.json index 25a8cb4766..081b41a9b4 100644 --- a/app/packages/playback/package.json +++ b/app/packages/playback/package.json @@ -1,5 +1,6 @@ { "name": "@fiftyone/playback", + "main": "./index.ts", "packageManager": "yarn@3.2.1", "devDependencies": { "@eslint/compat": "^1.1.1", diff --git a/app/yarn.lock b/app/yarn.lock index 3e82586cb0..dc9917db4b 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -13869,13 +13869,13 @@ __metadata: linkType: hard "postcss@npm:^8.4.43": - version: 8.4.44 - resolution: "postcss@npm:8.4.44" + version: 8.4.45 + resolution: "postcss@npm:8.4.45" dependencies: nanoid: ^3.3.7 picocolors: ^1.0.1 source-map-js: ^1.2.0 - checksum: 64d9ce78253696bb64e608a54b362c9ddb537d3b38b58223ebce8260d6110d4e798ef1b3d57d8c28131417d9809187fd51d5c4263113536363444f8635e11bdb + checksum: 3223cdad4a9392c0b334ee3ee7e4e8041c631cb6160609cef83c18d2b2580e931dd8068ab13cc6000c1a254d57492ac6c38717efc397c5dcc9756d06bc9c44f3 languageName: node linkType: hard diff --git a/fiftyone/operators/types.py b/fiftyone/operators/types.py index fd69f6cd62..a22bd01230 100644 --- a/fiftyone/operators/types.py +++ b/fiftyone/operators/types.py @@ -2399,6 +2399,20 @@ def to_json(self): } +class FrameLoaderView(View): + """Utility for loading frames and animated panels. + + Args: + timeline_id (None): the ID of the timeline to load + on_load (None): the operator to execute when the frame is loaded + on_error (None): the operator to execute when the frame fails to load + on_load_range (None): the operator to execute when the frame is loading + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + class Container(BaseType): """Represents a base container for a container types."""