From 436c6150f3953644e8811640b4bd23d3c9949c2f Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 15 Jan 2025 15:44:06 +0100 Subject: [PATCH] updates after meeting 2025-01-14 --- WIP-notes.md | 150 +++- definition/definition/GraphicsAPI.md | 10 +- definition/definition/README.md | 80 +- .../typescript/src/apis/graphicsAPI.ts | 164 ++-- .../derived/typescript/src/apis/serverAPI.ts | 482 +++++------ .../src/generated/graphics-manifest.d.ts | 225 ++++-- .../src/generated/renderer-manifest.d.ts | 225 ++++-- reference/graphics/basic/manifest.json | 4 +- reference/graphics/l3rd-name/manifest.json | 37 +- reference/graphics/minimal/graphic.mjs | 43 +- reference/graphics/minimal/manifest.json | 3 +- reference/servers/nodejs-basic/package.json | 2 +- .../src/managers/GraphicsStore.ts | 755 ++++++++++-------- reference/servers/nodejs-basic/src/server.ts | 73 +- .../servers/nodejs-basic/src/serverApi.ts | 180 +++-- scripts/run-everything.js | 12 + .../src/lib/graphic/verify.js | 2 +- tools/graphics-devtool/src/lib/sw-version.js | 2 +- tools/graphics-devtool/src/service-worker.js | 2 +- 19 files changed, 1393 insertions(+), 1058 deletions(-) diff --git a/WIP-notes.md b/WIP-notes.md index bc6cb3b..c6fdf4b 100644 --- a/WIP-notes.md +++ b/WIP-notes.md @@ -4,50 +4,41 @@ _This is a document where we can scribble down notes while working on this defin --- +- We should split the manifest into { info, manifest } -* We should split the manifest into { info, manifest } - -* Should we aim for frame accuracy? +- Should we aim for frame accuracy? Discussion: Probably not. HTML rendering is best-effort. ref: https://spxgc.tawk.help/article/help-config-renderer#bottom - - * Batch commands, to guarantee that they will be executed on the same frame in multiple Graphics - * Will it be enough to be able to batch "invokeAction" commands, or do we need to consider other commands? + - Batch commands, to guarantee that they will be executed on the same frame in multiple Graphics \* Will it be enough to be able to batch "invokeAction" commands, or do we need to consider other commands? Discussion: Probably not. - ## Uploading Graphic File - -* Can it be a zipped file? -* Can file size be a problem? -* Should the upload url contain :id/:version? (Since it can be read from the file itself) - +- Can it be a zipped file? +- Can file size be a problem? +- Should the upload url contain :id/:version? (Since it can be read from the file itself) ## Graphic -* Do we need to provide a base url to the GraphicInstance on load? So that it knows from where to load resources. -* Should the Graphic WebComponent be added to the DOM before or after calling the load() method? - +- Do we need to provide a base url to the GraphicInstance on load? So that it knows from where to load resources. +- Should the Graphic WebComponent be added to the DOM before or after calling the load() method? -* A Question about positioning: - Should we render Graphics in full frame or in a container? +- A Question about positioning: + Should we render Graphics in full frame or in a container? - Conclusion: Graphics should be rendered in full frame, ie they take up the entire screen. - If a Renderer wants to handle transition logic and advanced composition, it can do so by communicating with the Graphics using vendor specific methods. + Conclusion: Graphics should be rendered in full frame, ie they take up the entire screen. + If a Renderer wants to handle transition logic and advanced composition, it can do so by communicating with the Graphics using vendor specific methods. blog post: Need to communicate between difference Graphics? - use LocalStorage! -* How to handle non-realtime graphics? +- How to handle non-realtime graphics? -* Thumbnails? - -* Graphic Capabilities, Renderer properties - dimensions, GPU acceleration - -* Graphics in hierarchy? paths? +- Thumbnails? +- Graphic Capabilities, Renderer properties + dimensions, GPU acceleration +- Graphics in hierarchy? paths? export graphic - mainly used BY the Graphic itself in renderer @@ -56,8 +47,7 @@ RenderTarget - intentionally vague Renderer Status - vendor specific? Scope: - GraphicsInstance - OK. - +GraphicsInstance - OK. Should it be webComponent? We'd need to explain it well, and provide examples. @@ -70,12 +60,102 @@ Scope: Should we have the Renderer as a MUST question of scope: Will vendors implement separate Renderers? - - standard methods: - * play: "animate in" - * update: "update state" - * continue? - * step x? - * stop: "animate out" +standard methods: +_ play: "animate in" +_ update: "update state" +_ continue? +_ step x? \* stop: "animate out" * infer state-based data? + +Notes from 2015-01-15: + +- For non-realtime: in & out animation durations + AE: "Protected Regions" for in & outs + The durations are 99% fixed and wont change. + + it should be okay to add this to the manifest + +- AE uses "xxxLocalized" in their schema +- content credentials (for AI created content)? +- Style changes mid-show +- Redundancy + +Extensible graphic +Standard graphic + +manifest: +{ +actions: { +update: { // should be called before play +label: 'Update', +schema: { +data +} + + returns: { + stepCount: number + currentStep: number + } + }, + play: { + label: 'Play', + animationDuration: number + schema: null + }, + stop: { + label: 'Play', + animationDuration: number + schema: null // or obj + }, + + step: { // play is essentially a step to 1 + label: 'Step', + animationDuration: number + schema: { + type: 'object', + properties: { + // jump to step + // advance x steps forward/backward + // (no anination) + // play next step (with animation) + + absoluteStep?: number + deltaStep?: number + animate: boolean + } + } + }, + return { + currentStep: number // is 0 if step resulted in animate out + } + + + } + properties { + isStandardTemplate: true + stepCount: number + } + +} + +step() // continues +step(param) // step to that step + +continue(nextStep) // animates out, if its on the last step + +play==in +stop==out + +tuomos slides: +fire-and-forget +update+in +stepCount=0 +in-and-out in -- hold -- out +update+in+out +update+step(1)+step(0) +stepCount=1 +multi-step-template +update+in+next+next+next+out +update+in+next+out +stepCount=2+ diff --git a/definition/definition/GraphicsAPI.md b/definition/definition/GraphicsAPI.md index 775ce9d..48c1695 100644 --- a/definition/definition/GraphicsAPI.md +++ b/definition/definition/GraphicsAPI.md @@ -1,22 +1,20 @@ # Graphics API -The Graphics API is a javascript interface between the Renderer and the Graphics (a [Web Component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components)). +The Graphics API is a javascript interface between the **Renderer** and a Graphic (a [Web Component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components)). **TO BE WRITTEN. CURRENTLY A WORK IN PROGRESS CAN BE FOUND IN [../derived/typescript](../derived/typescript/README.md)** Graphics are rendered in full-frame, ie they take up the entire screen. - It is recommended that Graphics SHOULD be designed to be responsive, ie support multiple resolutions and aspect ratios. - ## Definition of a Graphic A Graphic consists of the following files: -* **manifest.json** - A JSON file containing metadata about the Graphic. See XYZ for more information. -* **graphic.mjs** - A javascript file containing the Graphic class. See XYZ for more information. -* **resources** - (optional) A folder containing any resources used by the Graphic, such as images, videos, fonts, etc. The resources folder MAY contain sub folders. +- **manifest.json** - A JSON file containing metadata about the Graphic. See XYZ for more information. +- **graphic.mjs** - A javascript file containing the Graphic class. See XYZ for more information. +- **resources** - (optional) A folder containing any resources used by the Graphic, such as images, videos, fonts, etc. The resources folder MAY contain sub folders. A Graphic MUST include the **manifest.json** and the **graphic.mjs** files (using these exact file names). A Graphic MAY include a **resources** folder, it can be omitted if the Graphic does not use any resources. diff --git a/definition/definition/README.md b/definition/definition/README.md index 3a96ab0..3ea0813 100644 --- a/definition/definition/README.md +++ b/definition/definition/README.md @@ -2,15 +2,10 @@ _This folder will contain the definitions in the form of text and JSON Schemas._ - - - - # Introduction & Overview ## Overview - The **Graphics Definition** is a specification to allow for interchangeable Graphics, Rendering Systems or Controllers in a broadcast environment. _For certain definitions, see the [Glossary](/definition/definition/Glossary.md)_. @@ -18,69 +13,70 @@ _For certain definitions, see the [Glossary](/definition/definition/Glossary.md) There are three main components in the **Graphics Definition**: ### The Editor + "A HTML template editor" -* A client or system where an end-user can create and edit **Graphics**. -* Uploads compiled **Graphics** to the **Server** + +- A client or system where an end-user can create and edit **Graphics**. +- Uploads compiled **Graphics** to the **Server** ### The Controller + "A user-facing interface" -* Discovers available **Graphics** using the **Server** -* Displays an interface to a user using information about the **Graphics** -* Sends playout commands to **Renderers** (via the **Server**) +- Discovers available **Graphics** using the **Server** +- Displays an interface to a user using information about the **Graphics** +- Sends playout commands to **Renderers** (via the **Server**) -### Server -"A web-server" -* Stores and exposes **Graphics** -* Exposes an API for discovering **Graphics** -* Exposes an API for controlling **Renderers** +### The Rendering System -### Renderer -"A Web page" -* Acts on playout commands from the Server -* Fetches **Graphics** from the Server and renders them in a DOM -* Is a "Web page" -* Holds the state of the Graphics +A rendering system generally consists of a web-server and a Renderer. -* +The Controllers and Editors connect to the web-server using the **Server API**, to handle things like -_Note: Some vendors may choose to bundle a Renderer / Server into a single system._ +- Managing **Graphics** (upload, delete etc). +- Discovering **Graphics** +- Controlling **Renderers** + +The Rendering system is responsible to handles one or more **Renderers**. A Renderer is essentially a web page that: +- Acts on playout commands from the Server +- Fetches **Graphics** from the Server and renders them in a DOM +- Is a "Web page" +- Holds the state of the Graphics + +_Note: Some vendors may choose to bundle a Renderer / Server into a single system._ ### Intention and Scope The **Graphics Definition** is created using the following principles: -* The **Graphics**, **Server**/**Renderer** and **Controllers** should be interchangeable. -* The Definition should _not_ define the internals of the **Graphics**, **Renderers** or **Controllers** - that's up to the vendors. -* The Definition should define the _interfaces_ between the **Graphics**, **Renderers** and **Controllers**. -* The Definition should allow for vendors to extend the defined APIs to provide vendor-specific functionality while still being compatible with the Definition. -* The Definition strives to be forwards and backwards compatible. +- The **Graphics**, **Rendering system** and **Controllers** are interchangeable. +- The Definition should _not_ define the internals of the **Graphics**, **Rendering system** nor **Controllers** - that's up to the vendors. +- The Definition should define the _interfaces_ between the **Graphics**, **Rendering system** and **Controllers**. +- The Definition should allow for vendors to extend the defined APIs to provide vendor-specific functionality while still being compatible with the Definition. +- The Definition strives to be forwards and backwards compatible. ### About the definitions The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). - ### API Definitions -The **Graphics Definition** consists of the following parts: - -* Server API ([LINK](/definition/definition/ServerAPI.md)) -* Renderer API ([LINK](/definition/definition/RendererAPI.md)) -* Graphics API ([LINK](/definition/definition/GraphicsAPI.md)) - - - - +The **Graphics Definition** consists of these two parts: +- Server API ([LINK](/definition/definition/ServerAPI.md)) +- Graphics API ([LINK](/definition/definition/GraphicsAPI.md)) ## To Vendors -Looking to implement a Graphic, a Server/Renderers or a Controller? +Looking to implement a Graphic, a Rendering system or a Controller? Here are a few tips and tricks useful for you! -* Looking to extend the API? To ensure that you're compatible with future versions of the Graphic Definition, **always prefix your methods, properties or endpoints** with "_VENDORNAME" (ie beginning with underscore, then your vendor name). -* Look at the reference implementations for inspiration. -* Use the tools (TBD) to verify that your +- Looking to extend the API? To ensure that you're compatible with future versions of the Graphic Definition, **always prefix your methods, properties or endpoints** with "\_VENDORNAME" (ie beginning with underscore, then your vendor name). +- Look at the reference implementations for inspiration. + +### For Graphics Developers + +Looking to develop a **Graphic**? Read this Getting Started Guide. +- Use the tools (TBD) to verify that your diff --git a/definition/derived/typescript/src/apis/graphicsAPI.ts b/definition/derived/typescript/src/apis/graphicsAPI.ts index 18e84f8..68718fe 100644 --- a/definition/derived/typescript/src/apis/graphicsAPI.ts +++ b/definition/derived/typescript/src/apis/graphicsAPI.ts @@ -1,6 +1,6 @@ -import { GraphicLoadPayload } from "../definitions/graphic" -import { GraphicInstanceStatus } from "../definitions/graphicInstance" -import { ActionInvokePayload, EmptyPayload } from "../definitions/types" +import { GraphicLoadPayload } from "../definitions/graphic"; +import { GraphicInstanceStatus } from "../definitions/graphicInstance"; +import { ActionInvokePayload, EmptyPayload } from "../definitions/types"; /** * ================================================================================================ @@ -10,78 +10,70 @@ import { ActionInvokePayload, EmptyPayload } from "../definitions/types" * ================================================================================================ */ - /** * Methods called on a GraphicInstance by the Renderer * @throws GraphicsError */ export interface GraphicsApi { + /** + * Called by the Renderer when the Graphic has been loaded into the DOM + * @returns a Promise that resolves when the Graphic has finished loading it's resources. + */ + load: (payload: GraphicLoadPayload) => Promise; + + /** + * Called by the Renderer to force the Graphic to terminate/dispose/clear any loaded resources. + * This is called after the Renderer has unloaded the Graphic from the DOM. + */ + dispose: (payload: EmptyPayload) => Promise; + + /** + * Called by the Renderer to retrieve the current status of the Graphic + */ + getStatus: (payload: EmptyPayload) => Promise; + + /** + * Called by the Renderer to invoke an Action on the Graphic + * @returns The return value of the invoked method (vendor-specific) + */ + invokeAction: (payload: ActionInvokePayload) => Promise; + + /** + * If the Graphic supports non-realtime rendering, this is called to make the graphic jump to a certain point in time. + * @returns A Promise that resolves when the Graphic has finished rendering the requested frame. + */ + goToTime: (payload: { timestamp: number }) => Promise; + + /** + * If the Graphic supports non-realtime rendering, this is called to schedule actions to be invoked at a certain point in time. + * When this is called, the Graphic is expected to store the scheduled actions and invoke them when the time comes. + * (A call to this replaces any previous scheduled actions.) + * @returns A Promise that resolves when the Graphic has stored the scheduled actions. + */ + setInvokeActionsSchedule: (payload: { /** - * Called by the Renderer when the Graphic has been loaded into the DOM - * @returns a Promise that resolves when the Graphic has finished loading it's resources. - */ - load: (payload: GraphicLoadPayload) => Promise - - /** - * Called by the Renderer to force the Graphic to terminate/dispose/clear any loaded resources. - * This is called after the Renderer has unloaded the Graphic from the DOM. - */ - dispose: (payload: EmptyPayload) => Promise - - /** - * Called by the Renderer to retrieve the current status of the Graphic - */ - getStatus: (payload: EmptyPayload) => Promise - - /** - * Called by the Renderer to invoke an Action on the Graphic - * @returns The return value of the invoked method (vendor-specific) - */ - invokeAction: (payload: ActionInvokePayload) => Promise - - /** - * If the Graphic supports non-realtime rendering, this is called to make the graphic jump to a certain point in time. - * @returns A Promise that resolves when the Graphic has finished rendering the requested frame. - */ - goToTime: (payload: { - timestamp: number - }) => Promise - - /** - * If the Graphic supports non-realtime rendering, this is called to schedule actions to be invoked at a certain point in time. - * When this is called, the Graphic is expected to store the scheduled actions and invoke them when the time comes. - * (A call to this replaces any previous scheduled actions.) - * @returns A Promise that resolves when the Graphic has stored the scheduled actions. + * A list of the scheduled actions to invoke at a certain point in time. */ - setInvokeActionsSchedule: (payload: { - /** - * A list of the scheduled actions to invoke at a certain point in time. - */ - schedule: { - timestamp: number - invokeAction: ActionInvokePayload - }[] - }) => Promise + schedule: { + timestamp: number; + invokeAction: ActionInvokePayload; + }[]; + }) => Promise; } - - - /** * Methods called on a Renderer by the GraphicInstance * @throws GraphicsError -*/ + */ export interface GraphicsRendererApi { - - /** Called when the GI has loaded all its resources and is ready to receive commands */ - loaded: () => void - /** Request to the Renderer to unload/kill the GraphicInstance */ - unload: () => void - /** Inform the Renderer that the GraphicInstance status has changed */ - status: (status: GraphicInstanceStatus) => void - /** Debugging information (for developers) */ - debug: (debugMessage: string) => void - + /** Called when the GI has loaded all its resources and is ready to receive commands */ + loaded: () => void; + /** Request to the Renderer to unload/kill the GraphicInstance */ + unload: () => void; + /** Inform the Renderer that the GraphicInstance status has changed */ + status: (status: GraphicInstanceStatus) => void; + /** Debugging information (for developers) */ + debug: (debugMessage: string) => void; } /** @@ -89,30 +81,30 @@ export interface GraphicsRendererApi { * Based on https://www.jsonrpc.org/specification#error_object */ export class GraphicsError extends Error { + constructor( + /** + * A Number that indicates the error type that occurred (404 not found, 500 internal error etc) + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + * @see https://www.jsonrpc.org/specification#error_object + */ + public readonly code: number, + /** + * A String providing a short description of the error. + * The message SHOULD be limited to a concise single sentence. + */ + message: string, + /** + * A Primitive or Structured value that contains additional information about the error. + * This may be omitted. + */ + public readonly data?: unknown + ) { + super(message); - constructor( - /** - * A Number that indicates the error type that occurred (404 not found, 500 internal error etc) - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status - * @see https://www.jsonrpc.org/specification#error_object - */ - public readonly code: number, - /** - * A String providing a short description of the error. - * The message SHOULD be limited to a concise single sentence. - */ - message: string, - /** - * A Primitive or Structured value that contains additional information about the error. - * This may be omitted. - */ - public readonly data?: unknown - ) { - super(message) - - if (!Number.isInteger(this.code)) throw new Error("code must be an integer") + if (!Number.isInteger(this.code)) + throw new Error("code must be an integer"); - // Set the prototype explicitly. - Object.setPrototypeOf(this, GraphicsError.prototype) - } + // Set the prototype explicitly. + Object.setPrototypeOf(this, GraphicsError.prototype); + } } diff --git a/definition/derived/typescript/src/apis/serverAPI.ts b/definition/derived/typescript/src/apis/serverAPI.ts index bb50d7e..43a2f99 100644 --- a/definition/derived/typescript/src/apis/serverAPI.ts +++ b/definition/derived/typescript/src/apis/serverAPI.ts @@ -1,20 +1,20 @@ import { - GraphicInfo, - GraphicInvokeActionTarget, - GraphicManifest -} from "../definitions/graphic" + GraphicInfo, + GraphicInvokeActionTarget, + GraphicManifest, +} from "../definitions/graphic"; import { - RendererInfo, - RendererManifest, - GraphicInstance, - RendererStatus, - RenderTargetStatus, - RendererLoadGraphicPayload, - RendererClearGraphicPayload, - GraphicInstanceOnTarget -} from "../definitions/renderer" -import { ActionInvokePayload, EmptyPayload } from "../definitions/types" -import { VendorSpecific } from "../definitions/vendor" + RendererInfo, + RendererManifest, + GraphicInstance, + RendererStatus, + RenderTargetStatus, + RendererLoadGraphicPayload, + RendererClearGraphicPayload, + GraphicInstanceOnTarget, +} from "../definitions/renderer"; +import { ActionInvokePayload, EmptyPayload } from "../definitions/types"; +import { VendorSpecific } from "../definitions/vendor"; /* * ================================================================================================ @@ -25,236 +25,252 @@ import { VendorSpecific } from "../definitions/vendor" * The Server SHOULD serve the API on the port 80 / 443 (but other ports are allowed) * * ================================================================================================ -*/ - + */ export interface Endpoints { - - /** A list of available graphics */ - listGraphics: { - method: 'GET', - path: '/serverApi/v1/graphics/list', - params: {}, - body: EmptyPayload, - returnValue: { - graphics: GraphicInfo[] - [vendorSpecific: VendorSpecific]: unknown - } | ErrorReturnValue, - } - - /** Delete a Graphic */ - deleteGraphic: { - method: 'DELETE', - path: '/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion', - params: { graphicId: string, graphicVersion: string }, - body: { - /** - * Whether to force deletion - * If force is false, it is recommended that the server keeps the Graphic for a while, but unlist it. - * This is to ensure that any currently-on-air Graphics are not affected. - */ - force?: boolean - }, - returnValue: EmptyPayload | ErrorReturnValue, - } - /** Returns info of a Graphic (manifest etc) */ - getGraphicManifest: { - method: 'GET', - path: '/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion/manifest', - params: { graphicId: string, graphicVersion: string }, - body: EmptyPayload, - returnValue: { - graphicManifest: (GraphicInfo & GraphicManifest) | undefined - [vendorSpecific: VendorSpecific]: unknown - } | ErrorReturnValue, - } - /** - * Returns the javascript file for a Graphic (ie the graphic.mjs file of a graphic) - */ - getGraphicModule: { - method: 'GET', - path: '/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion/graphic', - params: { graphicId: string, graphicVersion: string }, - body: EmptyPayload, - returnValue: { - // The contents of the graphic.mjs file - } | ErrorReturnValue, - } - /** - * Returns any of the resources from the /resources folder of a Graphic - */ - getGraphicResource: { - method: 'GET', - path: '/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion/resources/:localPath', - params: { graphicId: string, graphicVersion: string, localPath: string }, - body: EmptyPayload, - returnValue: { - // The contents of the requested resource file - } | ErrorReturnValue, - } - /** - * Upload a Graphic. - * The Graphic is uploaded as a zip file in multi-part mode. - */ - uploadGraphic: { - method: 'POST', - path: '/serverApi/v1/graphics/graphic', - params: {}, - body: EmptyPayload, - returnValue: EmptyPayload | ErrorReturnValue, - } - - - - /** - * Return a list of Renderers - */ - listRenderers: { - method: 'GET', - path: '/serverApi/v1/renderers/list', - params: {}, - body: EmptyPayload, - returnValue: { - renderers: RendererInfo[] - [vendorSpecific: VendorSpecific]: unknown - } | ErrorReturnValue, - } - /** - * Returns the manifest for a Renderer - */ - getRendererManifest: { - method: 'GET', - path: '/serverApi/v1/renderers/renderer/:rendererId/manifest', - params: { rendererId: string }, - body: EmptyPayload, - returnValue: { - rendererManifest: RendererInfo & RendererManifest - [vendorSpecific: VendorSpecific]: unknown - } | ErrorReturnValue, - } - // /** - // * Returns a list of GraphicInstances on a RenderTarget - // */ - // listRenderTargetGraphicInstances: { - // method: 'GET', - // path: '/serverApi/v1/renderers/renderer/:rendererId/graphicInstances', - // params: { rendererId: string }, - // body: EmptyPayload, - // returnValue: { - // graphicInstances: GraphicInstance[] - // [vendorSpecific: VendorSpecific]: unknown - // } | ErrorReturnValue, - // } - /** - * Returns the status of a Renderer - */ - getRendererStatus: { - method: 'GET', - path: '/serverApi/v1/renderers/renderer/:rendererId/status', - params: { rendererId: string }, - body: EmptyPayload, - returnValue: { - status: RendererStatus - [vendorSpecific: VendorSpecific]: unknown - } | ErrorReturnValue, - } - /** - * Returns the status of a RenderTarget - */ - getRenderTargetStatus: { - method: 'GET', - path: '/serverApi/v1/renderers/renderer/:rendererId/target/:renderTargetId/status', - params: { rendererId: string, renderTargetId: string }, - body: EmptyPayload, - returnValue: { - renderTargetStatus: RenderTargetStatus - [vendorSpecific: VendorSpecific]: unknown - } | ErrorReturnValue, - } - /** - * Invoke an action on the Renderer - * Available actions are defined in the Renderer's manifest. - * Returns the result of the action, or 404 if the acton is not found. - */ - invokeRendererAction: { - method: 'POST', - path: '/serverApi/v1/renderers/renderer/:rendererId/invokeAction', - params: { rendererId: string }, - body: { action: ActionInvokePayload}, - returnValue: { - /** Value returned by the action */ - value: unknown - [vendorSpecific: VendorSpecific]: unknown - } | ErrorReturnValue, - } - /** - * Instructs a Renderer to load a Graphic onto a RenderTarget - */ - loadGraphic: { - method: 'POST', - path: '/serverApi/v1/renderers/renderer/:rendererId/target/:renderTargetId/load', - params: { rendererId: string, renderTargetId: string }, - body: RendererLoadGraphicPayload, - returnValue: { - /** - * A reference to the loaded GraphicInstance. - */ - graphicInstanceId: string - [vendorSpecific: VendorSpecific]: unknown - } | ErrorReturnValue, - } - /** - * Instructs a Renderer to clear Graphics (using filters in payload) - */ - clearGraphic: { - method: 'POST', - path: '/serverApi/v1/renderers/renderer/:rendererId/clear', - params: { rendererId: string }, - body: RendererClearGraphicPayload, - returnValue: { - /** A list of the clearedGraphicsInstances */ - graphicInstance: GraphicInstanceOnTarget[] - [vendorSpecific: VendorSpecific]: unknown - } | ErrorReturnValue, - } - /** - * Invoke an action on a GraphicInstance - * Available actions are defined in the Graphic's manifest. - * Returns the result of the action, or 404 if the acton is not found. - */ - invokeGraphicAction: { - method: 'POST', - path: '/serverApi/v1/renderers/renderer/:rendererId/target/:renderTargetId/invokeAction', - params: { rendererId: string, renderTargetId: string }, - body: { - target: GraphicInvokeActionTarget - action: ActionInvokePayload - [vendorSpecific: VendorSpecific]: unknown + /** A list of available graphics */ + listGraphics: { + method: "GET"; + path: "/serverApi/v1/graphics/list"; + params: {}; + body: EmptyPayload; + returnValue: + | { + graphics: GraphicInfo[]; + [vendorSpecific: VendorSpecific]: unknown; } - returnValue: { - /** Value returned by the action */ - value: unknown - [vendorSpecific: VendorSpecific]: unknown - } | ErrorReturnValue, - } + | ErrorReturnValue; + }; + /** Delete a Graphic */ + deleteGraphic: { + method: "DELETE"; + path: "/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion"; + params: { graphicId: string; graphicVersion: string }; + body: { + /** + * Whether to force deletion + * If force is false, it is recommended that the server keeps the Graphic for a while, but unlist it. + * This is to ensure that any currently-on-air Graphics are not affected. + */ + force?: boolean; + }; + returnValue: EmptyPayload | ErrorReturnValue; + }; + /** Returns info of a Graphic (manifest etc) */ + getGraphicManifest: { + method: "GET"; + path: "/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion/manifest"; + params: { graphicId: string; graphicVersion: string }; + body: EmptyPayload; + returnValue: + | { + graphicManifest: (GraphicInfo & GraphicManifest) | undefined; + [vendorSpecific: VendorSpecific]: unknown; + } + | ErrorReturnValue; + }; + /** + * Returns the javascript file for a Graphic (ie the graphic.mjs file of a graphic) + */ + getGraphicModule: { + method: "GET"; + path: "/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion/graphic"; + params: { graphicId: string; graphicVersion: string }; + body: EmptyPayload; + returnValue: + | { + // The contents of the graphic.mjs file + } + | ErrorReturnValue; + }; + /** + * Returns any of the resources from the /resources folder of a Graphic + */ + getGraphicResource: { + method: "GET"; + path: "/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion/resources/:localPath*"; + params: { graphicId: string; graphicVersion: string; localPath: string }; + body: EmptyPayload; + returnValue: + | { + // The contents of the requested resource file + } + | ErrorReturnValue; + }; + /** + * Upload a Graphic. + * The Graphic is uploaded as a zip file in multi-part mode. + */ + uploadGraphic: { + method: "POST"; + path: "/serverApi/v1/graphics/graphic"; + params: {}; + body: EmptyPayload; + returnValue: EmptyPayload | ErrorReturnValue; + }; + /** + * Return a list of Renderers + */ + listRenderers: { + method: "GET"; + path: "/serverApi/v1/renderers/list"; + params: {}; + body: EmptyPayload; + returnValue: + | { + renderers: RendererInfo[]; + [vendorSpecific: VendorSpecific]: unknown; + } + | ErrorReturnValue; + }; + /** + * Returns the manifest for a Renderer + */ + getRendererManifest: { + method: "GET"; + path: "/serverApi/v1/renderers/renderer/:rendererId/manifest"; + params: { rendererId: string }; + body: EmptyPayload; + returnValue: + | { + rendererManifest: RendererInfo & RendererManifest; + [vendorSpecific: VendorSpecific]: unknown; + } + | ErrorReturnValue; + }; + // /** + // * Returns a list of GraphicInstances on a RenderTarget + // */ + // listRenderTargetGraphicInstances: { + // method: 'GET', + // path: '/serverApi/v1/renderers/renderer/:rendererId/graphicInstances', + // params: { rendererId: string }, + // body: EmptyPayload, + // returnValue: { + // graphicInstances: GraphicInstance[] + // [vendorSpecific: VendorSpecific]: unknown + // } | ErrorReturnValue, + // } + /** + * Returns the status of a Renderer + */ + getRendererStatus: { + method: "GET"; + path: "/serverApi/v1/renderers/renderer/:rendererId/status"; + params: { rendererId: string }; + body: EmptyPayload; + returnValue: + | { + status: RendererStatus; + [vendorSpecific: VendorSpecific]: unknown; + } + | ErrorReturnValue; + }; + /** + * Returns the status of a RenderTarget + */ + getRenderTargetStatus: { + method: "GET"; + path: "/serverApi/v1/renderers/renderer/:rendererId/target/:renderTargetId/status"; + params: { rendererId: string; renderTargetId: string }; + body: EmptyPayload; + returnValue: + | { + renderTargetStatus: RenderTargetStatus; + [vendorSpecific: VendorSpecific]: unknown; + } + | ErrorReturnValue; + }; + /** + * Invoke an action on the Renderer + * Available actions are defined in the Renderer's manifest. + * Returns the result of the action, or 404 if the acton is not found. + */ + invokeRendererAction: { + method: "POST"; + path: "/serverApi/v1/renderers/renderer/:rendererId/invokeAction"; + params: { rendererId: string }; + body: { action: ActionInvokePayload }; + returnValue: + | { + /** Value returned by the action */ + value: unknown; + [vendorSpecific: VendorSpecific]: unknown; + } + | ErrorReturnValue; + }; + /** + * Instructs a Renderer to load a Graphic onto a RenderTarget + */ + loadGraphic: { + method: "POST"; + path: "/serverApi/v1/renderers/renderer/:rendererId/target/:renderTargetId/load"; + params: { rendererId: string; renderTargetId: string }; + body: RendererLoadGraphicPayload; + returnValue: + | { + /** + * A reference to the loaded GraphicInstance. + */ + graphicInstanceId: string; + [vendorSpecific: VendorSpecific]: unknown; + } + | ErrorReturnValue; + }; + /** + * Instructs a Renderer to clear Graphics (using filters in payload) + */ + clearGraphic: { + method: "POST"; + path: "/serverApi/v1/renderers/renderer/:rendererId/clear"; + params: { rendererId: string }; + body: RendererClearGraphicPayload; + returnValue: + | { + /** A list of the clearedGraphicsInstances */ + graphicInstance: GraphicInstanceOnTarget[]; + [vendorSpecific: VendorSpecific]: unknown; + } + | ErrorReturnValue; + }; + /** + * Invoke an action on a GraphicInstance + * Available actions are defined in the Graphic's manifest. + * Returns the result of the action, or 404 if the acton is not found. + */ + invokeGraphicAction: { + method: "POST"; + path: "/serverApi/v1/renderers/renderer/:rendererId/target/:renderTargetId/invokeAction"; + params: { rendererId: string; renderTargetId: string }; + body: { + target: GraphicInvokeActionTarget; + action: ActionInvokePayload; + [vendorSpecific: VendorSpecific]: unknown; + }; + returnValue: + | { + /** Value returned by the action */ + value: unknown; + [vendorSpecific: VendorSpecific]: unknown; + } + | ErrorReturnValue; + }; } - // Helper types: -export type AnyPath = Endpoints[keyof Endpoints]['path'] -export type AnyBody = Endpoints[keyof Endpoints]['body'] -export type AnyReturnValue = Endpoints[keyof Endpoints]['returnValue'] - +export type AnyPath = Endpoints[keyof Endpoints]["path"]; +export type AnyBody = Endpoints[keyof Endpoints]["body"]; +export type AnyReturnValue = Endpoints[keyof Endpoints]["returnValue"]; /** * If there was an error when invoking a method, the body will be a JSON containing this structure. * @see https://www.jsonrpc.org/specification#error_object */ export interface ErrorReturnValue { - code: number - message: string - data?: any - [vendorSpecific: VendorSpecific]: unknown + code: number; + message: string; + data?: any; + [vendorSpecific: VendorSpecific]: unknown; } diff --git a/definition/derived/typescript/src/generated/graphics-manifest.d.ts b/definition/derived/typescript/src/generated/graphics-manifest.d.ts index f48f520..2ad3afa 100644 --- a/definition/derived/typescript/src/generated/graphics-manifest.d.ts +++ b/definition/derived/typescript/src/generated/graphics-manifest.d.ts @@ -19,72 +19,73 @@ export type HttpsSuperflytvGithubIoGraphicsDataDefinitionGddMetaSchemaV1LibObjec }; [k: string]: unknown; }; -export type CoreAndValidationSpecificationsMetaSchema = CoreVocabularyMetaSchema & - ApplicatorVocabularyMetaSchema & - UnevaluatedApplicatorVocabularyMetaSchema & - ValidationVocabularyMetaSchema & - MetaDataVocabularyMetaSchema & - FormatVocabularyMetaSchemaForAnnotationResults & - ContentVocabularyMetaSchema & { - /** - * @deprecated - */ - definitions?: { - [k: string]: { - [k: string]: unknown; +export type CoreAndValidationSpecificationsMetaSchema = + CoreVocabularyMetaSchema & + ApplicatorVocabularyMetaSchema & + UnevaluatedApplicatorVocabularyMetaSchema & + ValidationVocabularyMetaSchema & + MetaDataVocabularyMetaSchema & + FormatVocabularyMetaSchemaForAnnotationResults & + ContentVocabularyMetaSchema & { + /** + * @deprecated + */ + definitions?: { + [k: string]: { + [k: string]: unknown; + }; }; - }; - /** - * @deprecated - */ - dependencies?: { - [k: string]: - | { - [k: string]: unknown; - } - | string[]; - }; - /** - * @deprecated - */ - $recursiveAnchor?: string; - /** - * @deprecated - */ - $recursiveRef?: string; - [k: string]: unknown; - } & ( - | { - /** - * @deprecated - */ - definitions?: { - [k: string]: { - [k: string]: unknown; + /** + * @deprecated + */ + dependencies?: { + [k: string]: + | { + [k: string]: unknown; + } + | string[]; + }; + /** + * @deprecated + */ + $recursiveAnchor?: string; + /** + * @deprecated + */ + $recursiveRef?: string; + [k: string]: unknown; + } & ( + | { + /** + * @deprecated + */ + definitions?: { + [k: string]: { + [k: string]: unknown; + }; }; - }; - /** - * @deprecated - */ - dependencies?: { - [k: string]: - | { - [k: string]: unknown; - } - | string[]; - }; - /** - * @deprecated - */ - $recursiveAnchor?: string; - /** - * @deprecated - */ - $recursiveRef?: string; - [k: string]: unknown; - } - | boolean - ); + /** + * @deprecated + */ + dependencies?: { + [k: string]: + | { + [k: string]: unknown; + } + | string[]; + }; + /** + * @deprecated + */ + $recursiveAnchor?: string; + /** + * @deprecated + */ + $recursiveRef?: string; + [k: string]: unknown; + } + | boolean + ); export type CoreVocabularyMetaSchema = { $id?: string; $schema?: string; @@ -133,7 +134,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; items?: { [k: string]: unknown; @@ -180,7 +181,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; /** * @minItems 1 @@ -191,7 +192,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; /** * @minItems 1 @@ -202,7 +203,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; not?: { [k: string]: unknown; @@ -219,7 +220,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; items?: { [k: string]: unknown; @@ -266,7 +267,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; /** * @minItems 1 @@ -277,7 +278,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; /** * @minItems 1 @@ -288,7 +289,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; not?: { [k: string]: unknown; @@ -319,10 +320,34 @@ export type UnevaluatedApplicatorVocabularyMetaSchema = { ); export type ValidationVocabularyMetaSchema = { type?: - | ("array" | "boolean" | "integer" | "null" | "number" | "object" | "string") + | ( + | "array" + | "boolean" + | "integer" + | "null" + | "number" + | "object" + | "string" + ) | [ - "array" | "boolean" | "integer" | "null" | "number" | "object" | "string", - ...("array" | "boolean" | "integer" | "null" | "number" | "object" | "string")[] + ( + | "array" + | "boolean" + | "integer" + | "null" + | "number" + | "object" + | "string" + ), + ...( + | "array" + | "boolean" + | "integer" + | "null" + | "number" + | "object" + | "string" + )[], ]; const?: unknown; enum?: unknown[]; @@ -349,10 +374,34 @@ export type ValidationVocabularyMetaSchema = { } & ( | { type?: - | ("array" | "boolean" | "integer" | "null" | "number" | "object" | "string") + | ( + | "array" + | "boolean" + | "integer" + | "null" + | "number" + | "object" + | "string" + ) | [ - "array" | "boolean" | "integer" | "null" | "number" | "object" | "string", - ...("array" | "boolean" | "integer" | "null" | "number" | "object" | "string")[] + ( + | "array" + | "boolean" + | "integer" + | "null" + | "number" + | "object" + | "string" + ), + ...( + | "array" + | "boolean" + | "integer" + | "null" + | "number" + | "object" + | "string" + )[], ]; const?: unknown; enum?: unknown[]; @@ -429,18 +478,20 @@ export type ContentVocabularyMetaSchema = { } | boolean ); -export type HttpsSuperflytvGithubIoGraphicsDataDefinitionGddMetaSchemaV1LibGddTypesJson = { - [k: string]: unknown; -}; -export type HttpsSuperflytvGithubIoGraphicsDataDefinitionGddMetaSchemaV1LibBasicTypesJson = { - [k: string]: unknown; -}; +export type HttpsSuperflytvGithubIoGraphicsDataDefinitionGddMetaSchemaV1LibGddTypesJson = + { + [k: string]: unknown; + }; +export type HttpsSuperflytvGithubIoGraphicsDataDefinitionGddMetaSchemaV1LibBasicTypesJson = + { + [k: string]: unknown; + }; export interface HttpsSuperflytvGithubIoTmpGraphicsDefinitionDefinitionDefinitionJsonSchemaGraphicsManifestSchemaJson { /** * Reference to the JSON-schema */ - $schema?: "https://superflytv.github.io/tmp-GraphicsDefinition/definition/definition/json-schema/graphics-manifest/schema.json"; + $schema?: "https://superflytv.github.io/tmp-GraphicsDefinition/definition/definition/json-schema/v1/graphics-manifest/extensible/schema.json"; /** * The id of the Graphic uniquely identifies it. It is recommended to use a reverse domain name notation. For example: com.superflytv.my-lowerthird. */ @@ -483,7 +534,9 @@ export interface HttpsSuperflytvGithubIoTmpGraphicsDefinitionDefinitionDefinitio * Custom Actions that can be invoked on the Graphic. */ actions: { - [k: string]: HttpsSuperflytvGithubIoTmpGraphicsDefinitionDefinitionDefinitionJsonSchemaLibActionJson; + [ + k: string + ]: HttpsSuperflytvGithubIoTmpGraphicsDefinitionDefinitionDefinitionJsonSchemaLibActionJson; }; /** * Properties of the Graphic related to the Renderer diff --git a/definition/derived/typescript/src/generated/renderer-manifest.d.ts b/definition/derived/typescript/src/generated/renderer-manifest.d.ts index 89b9c46..a35f0c9 100644 --- a/definition/derived/typescript/src/generated/renderer-manifest.d.ts +++ b/definition/derived/typescript/src/generated/renderer-manifest.d.ts @@ -19,72 +19,73 @@ export type HttpsSuperflytvGithubIoGraphicsDataDefinitionGddMetaSchemaV1LibObjec }; [k: string]: unknown; }; -export type CoreAndValidationSpecificationsMetaSchema = CoreVocabularyMetaSchema & - ApplicatorVocabularyMetaSchema & - UnevaluatedApplicatorVocabularyMetaSchema & - ValidationVocabularyMetaSchema & - MetaDataVocabularyMetaSchema & - FormatVocabularyMetaSchemaForAnnotationResults & - ContentVocabularyMetaSchema & { - /** - * @deprecated - */ - definitions?: { - [k: string]: { - [k: string]: unknown; +export type CoreAndValidationSpecificationsMetaSchema = + CoreVocabularyMetaSchema & + ApplicatorVocabularyMetaSchema & + UnevaluatedApplicatorVocabularyMetaSchema & + ValidationVocabularyMetaSchema & + MetaDataVocabularyMetaSchema & + FormatVocabularyMetaSchemaForAnnotationResults & + ContentVocabularyMetaSchema & { + /** + * @deprecated + */ + definitions?: { + [k: string]: { + [k: string]: unknown; + }; }; - }; - /** - * @deprecated - */ - dependencies?: { - [k: string]: - | { - [k: string]: unknown; - } - | string[]; - }; - /** - * @deprecated - */ - $recursiveAnchor?: string; - /** - * @deprecated - */ - $recursiveRef?: string; - [k: string]: unknown; - } & ( - | { - /** - * @deprecated - */ - definitions?: { - [k: string]: { - [k: string]: unknown; + /** + * @deprecated + */ + dependencies?: { + [k: string]: + | { + [k: string]: unknown; + } + | string[]; + }; + /** + * @deprecated + */ + $recursiveAnchor?: string; + /** + * @deprecated + */ + $recursiveRef?: string; + [k: string]: unknown; + } & ( + | { + /** + * @deprecated + */ + definitions?: { + [k: string]: { + [k: string]: unknown; + }; }; - }; - /** - * @deprecated - */ - dependencies?: { - [k: string]: - | { - [k: string]: unknown; - } - | string[]; - }; - /** - * @deprecated - */ - $recursiveAnchor?: string; - /** - * @deprecated - */ - $recursiveRef?: string; - [k: string]: unknown; - } - | boolean - ); + /** + * @deprecated + */ + dependencies?: { + [k: string]: + | { + [k: string]: unknown; + } + | string[]; + }; + /** + * @deprecated + */ + $recursiveAnchor?: string; + /** + * @deprecated + */ + $recursiveRef?: string; + [k: string]: unknown; + } + | boolean + ); export type CoreVocabularyMetaSchema = { $id?: string; $schema?: string; @@ -133,7 +134,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; items?: { [k: string]: unknown; @@ -180,7 +181,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; /** * @minItems 1 @@ -191,7 +192,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; /** * @minItems 1 @@ -202,7 +203,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; not?: { [k: string]: unknown; @@ -219,7 +220,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; items?: { [k: string]: unknown; @@ -266,7 +267,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; /** * @minItems 1 @@ -277,7 +278,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; /** * @minItems 1 @@ -288,7 +289,7 @@ export type ApplicatorVocabularyMetaSchema = { }, ...{ [k: string]: unknown; - }[] + }[], ]; not?: { [k: string]: unknown; @@ -319,10 +320,34 @@ export type UnevaluatedApplicatorVocabularyMetaSchema = { ); export type ValidationVocabularyMetaSchema = { type?: - | ("array" | "boolean" | "integer" | "null" | "number" | "object" | "string") + | ( + | "array" + | "boolean" + | "integer" + | "null" + | "number" + | "object" + | "string" + ) | [ - "array" | "boolean" | "integer" | "null" | "number" | "object" | "string", - ...("array" | "boolean" | "integer" | "null" | "number" | "object" | "string")[] + ( + | "array" + | "boolean" + | "integer" + | "null" + | "number" + | "object" + | "string" + ), + ...( + | "array" + | "boolean" + | "integer" + | "null" + | "number" + | "object" + | "string" + )[], ]; const?: unknown; enum?: unknown[]; @@ -349,10 +374,34 @@ export type ValidationVocabularyMetaSchema = { } & ( | { type?: - | ("array" | "boolean" | "integer" | "null" | "number" | "object" | "string") + | ( + | "array" + | "boolean" + | "integer" + | "null" + | "number" + | "object" + | "string" + ) | [ - "array" | "boolean" | "integer" | "null" | "number" | "object" | "string", - ...("array" | "boolean" | "integer" | "null" | "number" | "object" | "string")[] + ( + | "array" + | "boolean" + | "integer" + | "null" + | "number" + | "object" + | "string" + ), + ...( + | "array" + | "boolean" + | "integer" + | "null" + | "number" + | "object" + | "string" + )[], ]; const?: unknown; enum?: unknown[]; @@ -429,18 +478,20 @@ export type ContentVocabularyMetaSchema = { } | boolean ); -export type HttpsSuperflytvGithubIoGraphicsDataDefinitionGddMetaSchemaV1LibGddTypesJson = { - [k: string]: unknown; -}; -export type HttpsSuperflytvGithubIoGraphicsDataDefinitionGddMetaSchemaV1LibBasicTypesJson = { - [k: string]: unknown; -}; +export type HttpsSuperflytvGithubIoGraphicsDataDefinitionGddMetaSchemaV1LibGddTypesJson = + { + [k: string]: unknown; + }; +export type HttpsSuperflytvGithubIoGraphicsDataDefinitionGddMetaSchemaV1LibBasicTypesJson = + { + [k: string]: unknown; + }; export interface HttpsSuperflytvGithubIoTmpGraphicsDefinitionDefinitionDefinitionJsonSchemaGraphicsManifestSchemaJson { /** * Reference to the JSON-schema */ - $schema?: "https://superflytv.github.io/tmp-GraphicsDefinition/definition/definition/json-schema/graphics-manifest/schema.json"; + $schema?: "https://superflytv.github.io/tmp-GraphicsDefinition/definition/definition/json-schema/v1/graphics-manifest/extensible/schema.json"; /** * The id of the Renderer. */ @@ -453,7 +504,9 @@ export interface HttpsSuperflytvGithubIoTmpGraphicsDefinitionDefinitionDefinitio * Custom Actions that can be invoked on the Renderer. */ actions: { - [k: string]: HttpsSuperflytvGithubIoTmpGraphicsDefinitionDefinitionDefinitionJsonSchemaLibActionJson; + [ + k: string + ]: HttpsSuperflytvGithubIoTmpGraphicsDefinitionDefinitionDefinitionJsonSchemaLibActionJson; }; /** * The different RenterTargets this Renderer can render to. diff --git a/reference/graphics/basic/manifest.json b/reference/graphics/basic/manifest.json index 6c572d0..9b80203 100644 --- a/reference/graphics/basic/manifest.json +++ b/reference/graphics/basic/manifest.json @@ -1,13 +1,11 @@ { - "$schema": "https://superflytv.github.io/tmp-GraphicsDefinition/definition/definition/json-schema/graphics-manifest/schema.json", + "$schema": "https://superflytv.github.io/tmp-GraphicsDefinition/definition/definition/json-schema/v1/graphics-manifest/extensible/schema.json", "name": "Minimal Test Graphic", "id": "minimal", "version": 0, - "actions": { "play": { "label": "Play", - "schema": { "type": "object", "properties": { diff --git a/reference/graphics/l3rd-name/manifest.json b/reference/graphics/l3rd-name/manifest.json index 3578145..f386e30 100644 --- a/reference/graphics/l3rd-name/manifest.json +++ b/reference/graphics/l3rd-name/manifest.json @@ -1,12 +1,12 @@ { - "$schema": "https://superflytv.github.io/tmp-GraphicsDefinition/definition/definition/json-schema/graphics-manifest/schema.json", + "$schema": "https://superflytv.github.io/tmp-GraphicsDefinition/definition/definition/json-schema/v1/graphics-manifest/schema.json", "name": "Lower 3rd - Name", "description": "Name lower third", "id": "l3rd-name", "version": 0, "actions": { - "play": { - "label": "Play", + "update": { + "label": "Update", "schema": { "type": "object", "properties": { @@ -18,13 +18,42 @@ } } }, + "play": { + "label": "Play", + "schema": null + }, "stop": { "label": "Stop", "schema": null + }, + "step": { + "label": "Step", + "schema": { + "type": "object", + "properties": { + "delta": { + "type": "number" + }, + "goto": { + "type": "number" + }, + "skipAnimation": { + "type": "boolean" + } + } + } + }, + "fire": { + "label": "Fire", + "schema": { + "type": "object", + "properties": {} + } } }, "rendering": { "supportsRealTime": true, - "supportsNonRealTime": true + "supportsNonRealTime": true, + "isStandardGraphic": true } } diff --git a/reference/graphics/minimal/graphic.mjs b/reference/graphics/minimal/graphic.mjs index 232b59f..0cc48fd 100644 --- a/reference/graphics/minimal/graphic.mjs +++ b/reference/graphics/minimal/graphic.mjs @@ -1,46 +1,47 @@ - class Graphic extends HTMLElement { - connectedCallback() { // Called when the element is added to the DOM // Note: Don't paint any pixels at this point, wait for load() to be called } async load(loadParams) { - - if (loadParams.renderType !== 'realtime') throw new Error('Only realtime rendering is supported') + if (loadParams.renderType !== "realtime") + throw new Error("Only realtime rendering is supported"); // Display an image - const image = document.createElement('img') - image.src = loadParams.baseUrl + '/resources/thumbs-up.jpg' - this.appendChild(image) + const image = document.createElement("img"); + image.src = loadParams.baseUrl + "/resources/thumbs-up.jpg"; + this.appendChild(image); - const iframe = document.createElement('iframe') - iframe.src = loadParams.baseUrl + '/resources/myIframe.html' - this.appendChild(iframe) + const iframe = document.createElement("iframe"); + this.elIframe = iframe; + iframe.src = loadParams.baseUrl + "/resources/myIframe.html"; + this.appendChild(iframe); // When everything is loaded we can return: - return + return; } - async dispose (_payload) { - this.innerHTML = '' + async dispose(_payload) { + this.innerHTML = ""; } - async getStatus (_payload) { - return {} + async getStatus(_payload) { + return {}; } - async invokeAction (params) { + async invokeAction(params) { // No actions are implemented in this minimal example // params.method, params.payload + // if (params.method === 'play') this.elIframe.contentWindow.play() + this.elIframe.contentWindow[params.method](params.payload); } - async goToTime (_payload) { - throw new Error('Non-realtime not supported!') + async goToTime(_payload) { + throw new Error("Non-realtime not supported!"); } - async setInvokeActionsSchedule (_payload) { - throw new Error('Non-realtime not supported!') + async setInvokeActionsSchedule(_payload) { + throw new Error("Non-realtime not supported!"); } } -export { Graphic } +export { Graphic }; // Note: The renderer will render the component diff --git a/reference/graphics/minimal/manifest.json b/reference/graphics/minimal/manifest.json index c87c33f..583dfaf 100644 --- a/reference/graphics/minimal/manifest.json +++ b/reference/graphics/minimal/manifest.json @@ -1,10 +1,9 @@ { - "$schema": "https://superflytv.github.io/tmp-GraphicsDefinition/definition/definition/json-schema/graphics-manifest/schema.json", + "$schema": "https://superflytv.github.io/tmp-GraphicsDefinition/definition/definition/json-schema/v1/graphics-manifest/extensible/schema.json", "name": "Minimal Test Graphic", "description": "This Graphic includes the bare minimum required to be a valid Graphic.", "id": "minimal", "version": 1, - "actions": {}, "rendering": { "supportsRealTime": true, diff --git a/reference/servers/nodejs-basic/package.json b/reference/servers/nodejs-basic/package.json index 62cc0f7..4c000c8 100644 --- a/reference/servers/nodejs-basic/package.json +++ b/reference/servers/nodejs-basic/package.json @@ -4,7 +4,7 @@ "main": "src/main.ts", "license": "MIT", "scripts": { - "dev": "nodemon src/main.ts" + "dev": "nodemon --watch src --verbose src/main.ts" }, "dependencies": { "@koa/cors": "^5.0.0", diff --git a/reference/servers/nodejs-basic/src/managers/GraphicsStore.ts b/reference/servers/nodejs-basic/src/managers/GraphicsStore.ts index 38e21f5..132dc9c 100644 --- a/reference/servers/nodejs-basic/src/managers/GraphicsStore.ts +++ b/reference/servers/nodejs-basic/src/managers/GraphicsStore.ts @@ -1,380 +1,449 @@ -import fs from "fs" -import mime from 'mime-types' -import path from "path" +import fs from "fs"; +import mime from "mime-types"; +import path from "path"; import { - ServerAPI, - GraphicInfo, - GraphicManifest -} from "html-graphics-definition" -import { CTX, literal } from "../lib/lib" -import decompress from "decompress" + ServerAPI, + GraphicInfo, + GraphicManifest, +} from "html-graphics-definition"; +import { CTX, literal } from "../lib/lib"; +import decompress from "decompress"; export class GraphicsStore { - - - /** File path where to store Graphics */ - private FILE_PATH = path.resolve('./localGraphicsStorage') - /** How long to wait befor removing Graphics, in ms */ - private REMOVAL_WAIT_TIME = 1000 * 3600 * 24 // 24 hours - - constructor () { - // Ensure the directory exists - fs.mkdirSync(this.FILE_PATH, { recursive: true }) - - setInterval(() => { - this.removeExpiredGraphics().catch(console.error) - }, 1000 * 3600 * 24) // Check every 24 hours - // Also do a check now: - this.removeExpiredGraphics().catch(console.error) + /** File path where to store Graphics */ + private FILE_PATH = path.resolve("./localGraphicsStorage"); + /** How long to wait befor removing Graphics, in ms */ + private REMOVAL_WAIT_TIME = 1000 * 3600 * 24; // 24 hours + + constructor() { + // Ensure the directory exists + fs.mkdirSync(this.FILE_PATH, { recursive: true }); + + setInterval(() => { + this.removeExpiredGraphics().catch(console.error); + }, 1000 * 3600 * 24); // Check every 24 hours + // Also do a check now: + this.removeExpiredGraphics().catch(console.error); + } + + async listGraphics(ctx: CTX): Promise { + const folderList = await fs.promises.readdir(this.FILE_PATH); + + const graphics: GraphicInfo[] = []; + for (const folder of folderList) { + const { id, version } = this.fromFileName(folder); + + // Don't list Graphics that are marked for removal: + if (await this.isGraphicMarkedForRemoval(id, version)) continue; + + const manifest = JSON.parse( + await fs.promises.readFile( + path.join(this.FILE_PATH, folder, "manifest.json"), + "utf8" + ) + ) as GraphicManifest & GraphicInfo; + + // Ensure the id and version match: + if (id !== manifest.id || version !== `${manifest.version}`) { + console.error( + `Folder name ${folder} does not match manifest id ${manifest.id} or version ${manifest.version}` + ); + continue; + } + + graphics.push({ + id: manifest.id, + version: manifest.version, + name: manifest.name, + description: manifest.description, + author: manifest.author, + draft: manifest.draft, + }); } - - async listGraphics(ctx: CTX): Promise { - - const folderList = (await fs.promises.readdir(this.FILE_PATH)) - - const graphics: GraphicInfo[] = [] - for (const folder of folderList) { - const {id, version} = this.fromFileName(folder) - - // Don't list Graphics that are marked for removal: - if (await this.isGraphicMarkedForRemoval(id, version)) continue - - const manifest = JSON.parse(await fs.promises.readFile(path.join(this.FILE_PATH, folder, 'manifest.json'), 'utf8')) as GraphicManifest & GraphicInfo - - // Ensure the id and version match: - if (id !== manifest.id || version !== `${manifest.version}`) { - console.error(`Folder name ${folder} does not match manifest id ${manifest.id} or version ${manifest.version}`) - continue - } - - graphics.push({ - id: manifest.id, - version: manifest.version, - name: manifest.name, - description: manifest.description, - author: manifest.author, - draft: manifest.draft, - }) + ctx.body = literal({ + graphics, + }); + } + async getGraphicManifest(ctx: CTX): Promise { + const params = + ctx.params as ServerAPI.Endpoints["getGraphicManifest"]["params"]; + const id = params.graphicId; + const version = params.graphicVersion; + + const manifestPath = path.join( + this.FILE_PATH, + this.toFileName(id, version), + "manifest.json" + ); + + // Don't return manifest if the Graphic is marked for removal: + if ( + (await this.fileExists(manifestPath)) && + !(await this.isGraphicMarkedForRemoval(id, version)) + ) { + const graphicManifest = JSON.parse( + await fs.promises.readFile(manifestPath, "utf8") + ) as GraphicManifest & GraphicInfo; + + // TODO + // graphicManifest.totalSize = + // graphicManifest.fileCount = + + if (graphicManifest) { + ctx.status = 200; + if (this.isImmutable(version)) { + ctx.header["Cache-Control"] = "public, max-age=31536000, immutable"; + } else { + // Never cache: + ctx.header["Cache-Control"] = "no-store"; } - ctx.body = literal({ graphics }) + ctx.body = literal< + ServerAPI.Endpoints["getGraphicManifest"]["returnValue"] + >({ graphicManifest }); + return; + } } - async getGraphicManifest(ctx: CTX): Promise { - const params = ctx.params as ServerAPI.Endpoints['getGraphicManifest']['params'] - const id = params.graphicId - const version = params.graphicVersion - - const manifestPath = path.join(this.FILE_PATH, this.toFileName(id, version), 'manifest.json') - - // Don't return manifest if the Graphic is marked for removal: - if (await this.fileExists(manifestPath) && !(await this.isGraphicMarkedForRemoval(id, version)) ) { - - const graphicManifest = JSON.parse(await fs.promises.readFile(manifestPath, 'utf8')) as GraphicManifest & GraphicInfo - - // TODO - // graphicManifest.totalSize = - // graphicManifest.fileCount = - - if (graphicManifest) { - ctx.status = 200 - if (this.isImmutable(version)) { - ctx.header['Cache-Control'] = 'public, max-age=31536000, immutable' - } else { - // Never cache: - ctx.header['Cache-Control'] = 'no-store' - } - ctx.body = literal({ graphicManifest }) - return - } - } - // else: - ctx.status = 404 - ctx.body = literal({ code: 404, message: `Graphic ${params.graphicId}-${params.graphicVersion} not found` }) - return + // else: + ctx.status = 404; + ctx.body = literal({ + code: 404, + message: `Graphic ${params.graphicId}-${params.graphicVersion} not found`, + }); + return; + } + async getGraphicModule(ctx: CTX): Promise { + const params = + ctx.params as ServerAPI.Endpoints["getGraphicModule"]["params"]; + const id: string = params.graphicId; + const version: string = params.graphicVersion; + + // Don't return graphic if the Graphic is marked for removal: + if (await this.isGraphicMarkedForRemoval(id, version)) { + ctx.status = 404; + ctx.body = literal({ + code: 404, + message: "File not found", + }); + return; } - async getGraphicModule(ctx: CTX): Promise { - const params = ctx.params as ServerAPI.Endpoints['getGraphicModule']['params'] - const id: string = params.graphicId - const version: string = params.graphicVersion - - // Don't return graphic if the Graphic is marked for removal: - if (await this.isGraphicMarkedForRemoval(id, version) ) { - ctx.status = 404 - ctx.body = literal({code: 404, message: 'File not found'}) - return - } - await this.serveFile(ctx, path.join(this.FILE_PATH, this.toFileName(id, version), 'graphic.mjs'), this.isImmutable(version)) + await this.serveFile( + ctx, + path.join(this.FILE_PATH, this.toFileName(id, version), "graphic.mjs"), + this.isImmutable(version) + ); + } + async getGraphicResource(ctx: CTX): Promise { + console.log("getGraphicResource"); + const params = + ctx.params as ServerAPI.Endpoints["getGraphicResource"]["params"]; + const id: string = params.graphicId; + const version: string = params.graphicVersion; + const localPath: string = params.localPath; + + console.log( + "url aaaa", + path.join( + this.FILE_PATH, + this.toFileName(id, version), + "resources", + localPath + ) + ); + + // Note: We DO serve resources even if the Graphic is marked for removal! + await this.serveFile( + ctx, + path.join( + this.FILE_PATH, + this.toFileName(id, version), + "resources", + localPath + ), + this.isImmutable(version) + ); + } + async deleteGraphic(ctx: CTX): Promise { + const params = ctx.params as ServerAPI.Endpoints["deleteGraphic"]["params"]; + const reqBody = ctx.request + .body as ServerAPI.Endpoints["deleteGraphic"]["body"]; + + if (reqBody.force) { + await this.actuallyDeleteGraphic(params.graphicId, params.graphicVersion); + } else { + await this.markGraphicForRemoval(params.graphicId, params.graphicVersion); } - async getGraphicResource(ctx: CTX): Promise { - const params = ctx.params as ServerAPI.Endpoints['getGraphicResource']['params'] - const id: string = params.graphicId - const version: string = params.graphicVersion - const localPath: string = params.localPath - - // Note: We DO serve resources even if the Graphic is marked for removal! - await this.serveFile(ctx, path.join(this.FILE_PATH, this.toFileName(id, version), 'resources', localPath), this.isImmutable(version)) + } + + async uploadGraphic(ctx: CTX): Promise { + // ctx.status = 501 + // ctx.body = literal({code: 501, message: 'Not implemented yet'}) + + // Expect a zipped file that contains the Graphic + const file = ctx.request.file; + // console.log('file', ctx.request.file) + // console.log('files', ctx.request.files) + // console.log('body', ctx.request.body) + + console.log("Uploaded file", file.originalname, file.size); + + if (file.mimetype !== "application/x-zip-compressed") { + ctx.status = 400; + ctx.body = literal({ + code: 400, + message: "Expected a zip file", + }); + return; } - async deleteGraphic(ctx: CTX): Promise { - const params = ctx.params as ServerAPI.Endpoints['deleteGraphic']['params'] - const reqBody = ctx.request.body as ServerAPI.Endpoints['deleteGraphic']['body'] - if (reqBody.force) { - await this.actuallyDeleteGraphic(params.graphicId, params.graphicVersion) - } else { - await this.markGraphicForRemoval(params.graphicId, params.graphicVersion) + const tempZipPath = file.path; + + const decompressPath = path.resolve("tmpGraphic"); + + const cleanup = async () => { + try { + await fs.promises.rm(decompressPath, { recursive: true }); + } catch (err: any) { + if (err.code !== "ENOENT") throw err; + } + }; + try { + await cleanup(); + + const files = await decompress(tempZipPath, decompressPath); + + const manifest = files.find((f) => f.path.endsWith("manifest.json")); + if (!manifest) throw new Error("No manifest.json found in zip file"); + + const graphicModule = files.find((f) => f.path.endsWith("graphic.mjs")); + if (!graphicModule) throw new Error("No graphic.mjs found in zip file"); + + let basePath = ""; + if (graphicModule.path.includes("/graphic.mjs")) { + // basepath/graphic.mjs + basePath = graphicModule.path.slice(0, -"/graphic.mjs".length); + } + + const manifestData = JSON.parse( + manifest.data.toString("utf8") + ) as GraphicManifest & GraphicInfo; + + const id = manifestData.id; + const version = `${manifestData.version}`; + const folderPath = path.join( + this.FILE_PATH, + this.toFileName(id, `${version}`) + ); + + // Check if the Graphic already exists + let alreadyExists = false; + if (await this.fileExists(folderPath)) { + alreadyExists = true; + + if (await this.isGraphicMarkedForRemoval(id, version)) { + // If a pre-existing graphic is marked for removal, we can overwrite it. + await this.actuallyDeleteGraphic(id, version); + alreadyExists = false; + } else if (version === "0") { + // If the version is 0, it is considered mutable, so we can overwrite it. + await this.actuallyDeleteGraphic(id, version); + alreadyExists = false; } - } - - async uploadGraphic(ctx: CTX): Promise { - - - - console.log('uploadGraphic') + } - // ctx.status = 501 - // ctx.body = literal({code: 501, message: 'Not implemented yet'}) + if (alreadyExists) { + await cleanup(); - // Expect a zipped file that contains the Graphic - const file = ctx.request.file - // console.log('file', ctx.request.file) - // console.log('files', ctx.request.files) - // console.log('body', ctx.request.body) + ctx.status = 409; // conflict + ctx.body = literal({ + code: 409, + message: "Graphic already exists", + }); + return; + } - console.log('Uploaded file',file.originalname, file.size) + // Copy the files to the right folder: + await fs.promises.mkdir(folderPath, { recursive: true }); - if (file.mimetype !== 'application/x-zip-compressed') { - ctx.status = 400 - ctx.body = literal({code: 400, message: 'Expected a zip file'}) - return - } - - const tempZipPath = file.path + // Then, copy files: + for (const innerFile of files) { + if (innerFile.type !== "file") continue; - const decompressPath = path.resolve('tmpGraphic') + const filePath = innerFile.path.slice(basePath.length); // Remove the base path - const cleanup = async () => { - try { - await fs.promises.rm(decompressPath, { recursive: true }) - } catch (err: any) { - if (err.code !== 'ENOENT') throw err - } - } + const outputFilePath = path.join(folderPath, filePath); + const outputFolderPath = path.dirname(outputFilePath); + // ensure dir: try { - await cleanup() - - - const files = await decompress(tempZipPath, decompressPath) - console.log('files', files) - - - - - const manifest = files.find(f => f.path.endsWith('manifest.json')) - if (!manifest) throw new Error('No manifest.json found in zip file') - - const graphicModule = files.find(f => f.path.endsWith('graphic.mjs')) - if (!graphicModule) throw new Error('No graphic.mjs found in zip file') - - let basePath = '' - if (graphicModule.path.includes('/graphic.mjs')) { // basepath/graphic.mjs - basePath = graphicModule.path.slice(0, -'/graphic.mjs'.length) - } - console.log('basePath', basePath) - - const manifestData = JSON.parse(manifest.data.toString('utf8')) as GraphicManifest & GraphicInfo - - const id = manifestData.id - const version = `${manifestData.version}` - const folderPath = path.join(this.FILE_PATH, this.toFileName(id, `${version}`)) - - // Check if the Graphic already exists - let alreadyExists = false - if (await this.fileExists(folderPath)) { - alreadyExists = true - - if (await this.isGraphicMarkedForRemoval(id, version)) { - // If a pre-existing graphic is marked for removal, we can overwrite it. - await this.actuallyDeleteGraphic(id, version) - alreadyExists = false - } else if (version === '0') { - // If the version is 0, it is considered mutable, so we can overwrite it. - await this.actuallyDeleteGraphic(id, version) - alreadyExists = false - } - } - if (alreadyExists) { - await cleanup() - - ctx.status = 409 // conflict - ctx.body = literal({code: 409, message: 'Graphic already exists'}) - return - } - - // Copy the files to the right folder: - await fs.promises.mkdir(folderPath, { recursive: true }) - - const fileName = path.join(this.FILE_PATH, this.toFileName(manifestData.id, `${manifestData.version}`)) - - - - for (const innerFile of files) { - if (innerFile.type !== 'directory') continue - - const filePath = innerFile.path.slice(basePath.length) // Remove the base path - try { - await fs.promises.mkdir(path.join(folderPath, filePath), { recursive: true }) - } catch (err) { - if (!`${err}`.includes('EEXIST')) throw err // Ignore "already exists" errors - } - - } - for (const innerFile of files) { - if (innerFile.type !== 'file') continue - - const filePath = innerFile.path.slice(basePath.length) // Remove the base path - await fs.promises.writeFile(path.join(folderPath, filePath), innerFile.data) - } - - ctx.status = 200 - ctx.body = literal({}) - } finally { - // clean up after ourselves: - await cleanup() + await fs.promises.mkdir(outputFolderPath, { + recursive: true, + }); + } catch (err) { + if (!`${err}`.includes("EEXIST")) throw err; // Ignore "already exists" errors } - } - - - - private async fileExists(filePath: string): Promise { - try { - await fs.promises.access(filePath) - return true - } - catch { - return false - } + // Copy data: + await fs.promises.writeFile(outputFilePath, innerFile.data); + } + + ctx.status = 200; + ctx.body = literal( + {} + ); + } finally { + // clean up after ourselves: + await cleanup(); } - - private toFileName(id: string, version: string) { - return `${id}-${version}` + } + + private async fileExists(filePath: string): Promise { + try { + await fs.promises.access(filePath); + return true; + } catch { + return false; } - private fromFileName(filename: string): {id: string, version: string} { - const p = filename.split('-') - if (p.length !== 2) throw new Error(`Invalid filename ${filename}`) - return {id: p[0], version: p[1]} + } + + private toFileName(id: string, version: string) { + return `${id}-${version}`; + } + private fromFileName(filename: string): { id: string; version: string } { + const m = filename.match(/(.+)-([^-]+)/); + if (!m) throw new Error(`Invalid filename ${filename}`); + return { id: m[1], version: m[2] }; + } + private isImmutable(version: string) { + // If the version is 0, the graphic is considered mutable + // ie, it is a non-production version, in development + // Otherwise it is considered immutable. + return `${version}` !== "0"; + } + + private async getFileInfo(filePath: string): Promise< + | { + found: false; + } + | { + found: true; + mimeType: string; + length: number; + lastModified: Date; + } + > { + if (!(await this.fileExists(filePath))) { + return { found: false }; } - private isImmutable(version: string) { - // If the version is 0, the graphic is considered mutable - // ie, it is a non-production version, in development - // Otherwise it is considered immutable. - return `${version}` !== '0' + let mimeType = mime.lookup(filePath); + if (!mimeType) { + // Fallback to "unknown binary": + mimeType = "application/octet-stream"; } - private async getFileInfo(filePath: string): Promise< - { - found: false - } | - { - found: true, - mimeType: string, - length: number, - lastModified: Date - } - > { - if (!(await this.fileExists(filePath))) { - return { found: false } - } - let mimeType = mime.lookup(filePath) - if (!mimeType) { - // Fallback to "unknown binary": - mimeType = 'application/octet-stream' - } - - const stat = await fs.promises.stat(filePath) - - return { - found: true, - mimeType, - length: stat.size, - lastModified: stat.mtime, - } + const stat = await fs.promises.stat(filePath); + + return { + found: true, + mimeType, + length: stat.size, + lastModified: stat.mtime, + }; + } + private async serveFile( + ctx: CTX, + fullPath: string, + immutable: boolean + ): Promise { + const info = await this.getFileInfo(fullPath); + + if (!info.found) { + ctx.status = 404; + ctx.body = literal({ + code: 404, + message: "File not found", + }); + return; } - private async serveFile(ctx: CTX, fullPath: string, immutable: boolean): Promise { - const info = await this.getFileInfo(fullPath) + ctx.type = info.mimeType; + ctx.length = info.length; + ctx.lastModified = info.lastModified; - if (!info.found) { - ctx.status = 404 - ctx.body = literal({code: 404, message: 'File not found'}) - return - } - - ctx.type = info.mimeType - ctx.length = info.length - ctx.lastModified = info.lastModified - - if (immutable) { - ctx.header['Cache-Control'] = 'public, max-age=31536000, immutable' - } else { - // Never cache: - ctx.header['Cache-Control'] = 'no-store' - } - - const readStream = fs.createReadStream(fullPath) - ctx.body = readStream as any + if (immutable) { + ctx.header["Cache-Control"] = "public, max-age=31536000, immutable"; + } else { + // Never cache: + ctx.header["Cache-Control"] = "no-store"; } - private async actuallyDeleteGraphic(id: string, version: string) { - const folderPath = path.join(this.FILE_PATH, this.toFileName(id, version)) + const readStream = fs.createReadStream(fullPath); + ctx.body = readStream as any; + } - if (await this.fileExists(folderPath)) { - await fs.promises.rm(folderPath, { recursive: true }) - } - } - private async markGraphicForRemoval(id: string, version: string) { - // Mark the Graphic for removal, but keep it for a while. - // The reason for this is to not delete a Graphic that is currently on-air - // (which might break due to missing resources) - - const folderPath = path.join(this.FILE_PATH, this.toFileName(id, version)) + private async actuallyDeleteGraphic(id: string, version: string) { + const folderPath = path.join(this.FILE_PATH, this.toFileName(id, version)); - const removalFilePath = path.join(folderPath, '__markedForRemoval') - - if (await this.fileExists(folderPath)) { - fs.promises.writeFile(removalFilePath, `${Date.now() + this.REMOVAL_WAIT_TIME}`, 'utf-8' ) - } + if (await this.fileExists(folderPath)) { + await fs.promises.rm(folderPath, { recursive: true }); } - /** Find any graphics that are due to be removed */ - private async removeExpiredGraphics() { - - const folderList = (await fs.promises.readdir(this.FILE_PATH)) - for (const folder of folderList) { - const {id, version} = this.fromFileName(folder) - - if (!(await this.isGraphicMarkedForRemoval(id, version))) continue - - const removalFilePath = path.join(this.FILE_PATH, folder, '__markedForRemoval') - - const removalTimeStr = await fs.promises.readFile(removalFilePath, 'utf-8') - const removalTime = parseInt(removalTimeStr) - if (Number.isNaN(removalTime)) { - console.error(`Invalid removal time in ${removalFilePath}`) - continue - } - - if (Date.now() > removalTime) { - // Time to remove the Graphic - await this.actuallyDeleteGraphic(id, version) - } - } + } + private async markGraphicForRemoval(id: string, version: string) { + // Mark the Graphic for removal, but keep it for a while. + // The reason for this is to not delete a Graphic that is currently on-air + // (which might break due to missing resources) + + const folderPath = path.join(this.FILE_PATH, this.toFileName(id, version)); + + const removalFilePath = path.join(folderPath, "__markedForRemoval"); + + if (await this.fileExists(folderPath)) { + fs.promises.writeFile( + removalFilePath, + `${Date.now() + this.REMOVAL_WAIT_TIME}`, + "utf-8" + ); } - - /** Returns true if a graphic exists (and is not marked for removal) */ - private async isGraphicMarkedForRemoval(id: string, version: string): Promise { - const removalFilePath = path.join(this.FILE_PATH, this.toFileName(id, version), '__markedForRemoval') - return await this.fileExists(removalFilePath) + } + /** Find any graphics that are due to be removed */ + private async removeExpiredGraphics() { + const folderList = await fs.promises.readdir(this.FILE_PATH); + for (const folder of folderList) { + const { id, version } = this.fromFileName(folder); + + if (!(await this.isGraphicMarkedForRemoval(id, version))) continue; + + const removalFilePath = path.join( + this.FILE_PATH, + folder, + "__markedForRemoval" + ); + + const removalTimeStr = await fs.promises.readFile( + removalFilePath, + "utf-8" + ); + const removalTime = parseInt(removalTimeStr); + if (Number.isNaN(removalTime)) { + continue; + } + + if (Date.now() > removalTime) { + // Time to remove the Graphic + await this.actuallyDeleteGraphic(id, version); + } } + } + + /** Returns true if a graphic exists (and is not marked for removal) */ + private async isGraphicMarkedForRemoval( + id: string, + version: string + ): Promise { + const removalFilePath = path.join( + this.FILE_PATH, + this.toFileName(id, version), + "__markedForRemoval" + ); + return await this.fileExists(removalFilePath); + } } - - diff --git a/reference/servers/nodejs-basic/src/server.ts b/reference/servers/nodejs-basic/src/server.ts index 7585714..c6eca09 100644 --- a/reference/servers/nodejs-basic/src/server.ts +++ b/reference/servers/nodejs-basic/src/server.ts @@ -1,40 +1,38 @@ -import Koa from 'koa' -import Router from "@koa/router" -import cors from '@koa/cors' -import bodyParser from 'koa-bodyparser' -import { setupServerApi } from './serverApi' -import { setupRendererApi } from './rendererApi' -import { KoaWsFilter } from '@zimtsui/koa-ws-filter' -import { GraphicsStore } from './managers/GraphicsStore' -import { RendererManager } from './managers/RendererManager' +import Koa from "koa"; +import Router from "@koa/router"; +import cors from "@koa/cors"; +import bodyParser from "koa-bodyparser"; +import { setupServerApi } from "./serverApi"; +import { setupRendererApi } from "./rendererApi"; +import { KoaWsFilter } from "@zimtsui/koa-ws-filter"; +import { GraphicsStore } from "./managers/GraphicsStore"; +import { RendererManager } from "./managers/RendererManager"; export async function initializeServer() { + const app = new Koa(); - const app = new Koa() + app.on("error", (err: unknown) => { + console.error(err); + }); + app.use(bodyParser()); - app.on('error', (err: unknown) => { - console.error(err) - }) - app.use(bodyParser()) + app.use(cors()); + // app.use(()) - app.use(cors()) + const httpRouter = new Router(); + const wsRouter = new Router(); + const filter = new KoaWsFilter(); - const httpRouter = new Router() - const wsRouter = new Router() - const filter = new KoaWsFilter () + // Initialize internal business logic + const graphicsStore = new GraphicsStore(); + const rendererManager = new RendererManager(); - // Initialize internal business logic - const graphicsStore = new GraphicsStore() - const rendererManager = new RendererManager() + // Setup APIs: + setupServerApi(httpRouter, graphicsStore, rendererManager); // HTTP API (ServerAPI) + setupRendererApi(wsRouter, rendererManager); // WebSocket API (RendererAPI) - // Setup APIs: - setupServerApi(httpRouter, graphicsStore, rendererManager) // HTTP API (ServerAPI) - setupRendererApi(wsRouter, rendererManager) // WebSocket API (RendererAPI) - - - - httpRouter.get('/', async (ctx) => { - ctx.body = ` + httpRouter.get("/", async (ctx) => { + ctx.body = `

NodeJS-based Graphics Server

    @@ -49,17 +47,16 @@ export async function initializeServer() { -` - }) - - filter.http(httpRouter.routes()) - filter.ws(wsRouter.routes()) +`; + }); + filter.http(httpRouter.routes()); + filter.ws(wsRouter.routes()); - app.use(filter.protocols()) + app.use(filter.protocols()); - const PORT = 8080 + const PORT = 8080; - app.listen(PORT) - console.log(`Server running on \x1b[36m http://127.0.0.1:${PORT}/\x1b[0m`) + app.listen(PORT); + console.log(`Server running on \x1b[36m http://127.0.0.1:${PORT}/\x1b[0m`); } diff --git a/reference/servers/nodejs-basic/src/serverApi.ts b/reference/servers/nodejs-basic/src/serverApi.ts index 83e0a6b..42eb9ca 100644 --- a/reference/servers/nodejs-basic/src/serverApi.ts +++ b/reference/servers/nodejs-basic/src/serverApi.ts @@ -1,79 +1,121 @@ - -import Koa from "koa" -import Router from "@koa/router" -import { GraphicsStore } from "./managers/GraphicsStore" -import { CTX, literal } from "./lib/lib" -import { ServerAPI } from "html-graphics-definition" -import multer from '@koa/multer' -import { RendererManager } from "./managers/RendererManager" +import Koa from "koa"; +import Router from "@koa/router"; +import { GraphicsStore } from "./managers/GraphicsStore"; +import { CTX, literal } from "./lib/lib"; +import { ServerAPI } from "html-graphics-definition"; +import multer from "@koa/multer"; +import { RendererManager } from "./managers/RendererManager"; const upload = multer({ - storage: multer.diskStorage({ - // destination: './localGraphicsStorage', - }) -}) - - -export function setupServerApi(router: Router, graphicsStore: GraphicsStore, rendererManager: RendererManager) { - - - // Make strong types for the path: - const serverApiRouter = { - get: (path: ServerAPI.AnyPath, ...middleware: (Koa.Middleware | ((ctx: CTX) => Promise))[]) => router.get(path, ...middleware), - post: (path: ServerAPI.AnyPath, ...middleware: (Koa.Middleware | ((ctx: CTX) => Promise))[]) => router.post(path, ...middleware), - put: (path: ServerAPI.AnyPath, ...middleware: (Koa.Middleware | ((ctx: CTX) => Promise))[]) => router.put(path, ...middleware), - delete: (path: ServerAPI.AnyPath, ...middleware: (Koa.Middleware | ((ctx: CTX) => Promise))[]) => router.delete(path, ...middleware), - } - // ----- Graphics related endpoints ------------------------------ - serverApiRouter.get(`/serverApi/v1/graphics/list`, handleError(async (ctx) => graphicsStore.listGraphics(ctx))) - serverApiRouter.delete(`/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion`, handleError(async (ctx) => graphicsStore.deleteGraphic(ctx))) - serverApiRouter.get(`/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion/manifest`, handleError(async (ctx) => graphicsStore.getGraphicManifest(ctx))) - serverApiRouter.get(`/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion/graphic`, handleError(async (ctx) => graphicsStore.getGraphicModule(ctx))) - serverApiRouter.get(`/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion/resources/:localPath`, handleError(async (ctx) => graphicsStore.getGraphicResource(ctx))) - serverApiRouter.post(`/serverApi/v1/graphics/graphic`, - upload.single('graphic'), - handleError(async (ctx) => graphicsStore.uploadGraphic(ctx)) - ) - - // ----- Renderer related endpoints -------------------------------- - serverApiRouter.get('/serverApi/v1/renderers/list', handleError(async (ctx) => rendererManager.listRenderers(ctx))) - serverApiRouter.get('/serverApi/v1/renderers/renderer/:rendererId/manifest', - handleError(async (ctx) => rendererManager.getRendererManifest(ctx)) - ) - serverApiRouter.get('/serverApi/v1/renderers/renderer/:rendererId/status', - handleError(async (ctx) => rendererManager.getRendererStatus(ctx) )) - serverApiRouter.get('/serverApi/v1/renderers/renderer/:rendererId/target/:renderTargetId/status', - handleError(async (ctx) => rendererManager.getRenderTargetStatus(ctx) )) - serverApiRouter.post('/serverApi/v1/renderers/renderer/:rendererId/invokeAction', - handleError(async (ctx) => rendererManager.invokeRendererAction(ctx) )) + storage: multer.diskStorage({ + // destination: './localGraphicsStorage', + }), +}); - serverApiRouter.post('/serverApi/v1/renderers/renderer/:rendererId/target/:renderTargetId/load', - handleError(async (ctx) => rendererManager.loadGraphic(ctx) )) - serverApiRouter.post('/serverApi/v1/renderers/renderer/:rendererId/clear', - handleError(async (ctx) => rendererManager.clearGraphic(ctx) )) - serverApiRouter.post('/serverApi/v1/renderers/renderer/:rendererId/target/:renderTargetId/invokeAction', - handleError(async (ctx) => rendererManager.invokeGraphicAction(ctx) )) +export function setupServerApi( + router: Router, + graphicsStore: GraphicsStore, + rendererManager: RendererManager +) { + // Make strong types for the path: + const serverApiRouter = { + get: ( + path: ServerAPI.AnyPath, + ...middleware: (Koa.Middleware | ((ctx: CTX) => Promise))[] + ) => router.get(path, ...middleware), + post: ( + path: ServerAPI.AnyPath, + ...middleware: (Koa.Middleware | ((ctx: CTX) => Promise))[] + ) => router.post(path, ...middleware), + put: ( + path: ServerAPI.AnyPath, + ...middleware: (Koa.Middleware | ((ctx: CTX) => Promise))[] + ) => router.put(path, ...middleware), + delete: ( + path: ServerAPI.AnyPath, + ...middleware: (Koa.Middleware | ((ctx: CTX) => Promise))[] + ) => router.delete(path, ...middleware), + }; + // ----- Graphics related endpoints ------------------------------ + serverApiRouter.get( + `/serverApi/v1/graphics/list`, + handleError(async (ctx) => graphicsStore.listGraphics(ctx)) + ); + serverApiRouter.delete( + `/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion`, + handleError(async (ctx) => graphicsStore.deleteGraphic(ctx)) + ); + serverApiRouter.get( + `/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion/manifest`, + handleError(async (ctx) => graphicsStore.getGraphicManifest(ctx)) + ); + serverApiRouter.get( + `/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion/graphic`, + handleError(async (ctx) => graphicsStore.getGraphicModule(ctx)) + ); + serverApiRouter.get( + `/serverApi/v1/graphics/graphic/:graphicId/:graphicVersion/resources/:localPath*`, + handleError(async (ctx) => graphicsStore.getGraphicResource(ctx)) + ); + serverApiRouter.post( + `/serverApi/v1/graphics/graphic`, + upload.single("graphic"), + handleError(async (ctx) => graphicsStore.uploadGraphic(ctx)) + ); + // ----- Renderer related endpoints -------------------------------- + serverApiRouter.get( + "/serverApi/v1/renderers/list", + handleError(async (ctx) => rendererManager.listRenderers(ctx)) + ); + serverApiRouter.get( + "/serverApi/v1/renderers/renderer/:rendererId/manifest", + handleError(async (ctx) => rendererManager.getRendererManifest(ctx)) + ); + serverApiRouter.get( + "/serverApi/v1/renderers/renderer/:rendererId/status", + handleError(async (ctx) => rendererManager.getRendererStatus(ctx)) + ); + serverApiRouter.get( + "/serverApi/v1/renderers/renderer/:rendererId/target/:renderTargetId/status", + handleError(async (ctx) => rendererManager.getRenderTargetStatus(ctx)) + ); + serverApiRouter.post( + "/serverApi/v1/renderers/renderer/:rendererId/invokeAction", + handleError(async (ctx) => rendererManager.invokeRendererAction(ctx)) + ); + serverApiRouter.post( + "/serverApi/v1/renderers/renderer/:rendererId/target/:renderTargetId/load", + handleError(async (ctx) => rendererManager.loadGraphic(ctx)) + ); + serverApiRouter.post( + "/serverApi/v1/renderers/renderer/:rendererId/clear", + handleError(async (ctx) => rendererManager.clearGraphic(ctx)) + ); + serverApiRouter.post( + "/serverApi/v1/renderers/renderer/:rendererId/target/:renderTargetId/invokeAction", + handleError(async (ctx) => rendererManager.invokeGraphicAction(ctx)) + ); } - - function handleError(fcn: (ctx: CTX) => Promise) { - return async (ctx: CTX) => { - - try { - await fcn(ctx) - } catch (err) { - console.error(err) - // Handle internal errors: - ctx.status = 500 - const body = literal({ code: 500, message: `Internal Error: ${err}`}) - ctx.body = body + return async (ctx: CTX) => { + try { + await fcn(ctx); + } catch (err) { + console.error(err); + // Handle internal errors: + ctx.status = 500; + const body = literal({ + code: 500, + message: `Internal Error: ${err}`, + }); + ctx.body = body; - if (err && typeof err === 'object' && err instanceof Error && err.stack) { - // Note: This is a security risk, as it exposes the stack trace to the client (don't do this in production) - body.data = { stack: err.stack } - } - } + if (err && typeof err === "object" && err instanceof Error && err.stack) { + // Note: This is a security risk, as it exposes the stack trace to the client (don't do this in production) + body.data = { stack: err.stack }; + } } + }; } diff --git a/scripts/run-everything.js b/scripts/run-everything.js index 3a33e36..c360be5 100644 --- a/scripts/run-everything.js +++ b/scripts/run-everything.js @@ -46,6 +46,12 @@ async function main() { cmd: "npm install", cwd: "reference/servers/nodejs-basic", }), + run({ + title: "Installing dependencies", + label: "renderers/browser-based-layered", + cmd: "npm install", + cwd: "reference/renderers/browser-based-layered", + }), run({ title: "Installing dependencies", label: "graphics-devtool", @@ -129,6 +135,12 @@ async function main() { cmd: "npm run dev", cwd: "reference/servers/nodejs-basic", }), + run({ + title: "Starting up development server", + label: "renderers/browser-based-layered", + cmd: "npm run start", + cwd: "reference/renderers/browser-based-layered", + }), run({ title: "Starting up development server", label: "graphics-devtool", diff --git a/tools/graphics-devtool/src/lib/graphic/verify.js b/tools/graphics-devtool/src/lib/graphic/verify.js index 31947cf..6695cfa 100644 --- a/tools/graphics-devtool/src/lib/graphic/verify.js +++ b/tools/graphics-devtool/src/lib/graphic/verify.js @@ -50,7 +50,7 @@ async function _setupSchemaValidator( const cache = options.getCache ? await options.getCache() : {} const baseURL = - 'https://superflytv.github.io/tmp-GraphicsDefinition/definition/definition/json-schema/graphics-manifest/schema.json' + 'https://superflytv.github.io/tmp-GraphicsDefinition/definition/definition/json-schema/v1/graphics-manifest/extensible/schema.json' const v = new Validator() async function addRef(ref) { diff --git a/tools/graphics-devtool/src/lib/sw-version.js b/tools/graphics-devtool/src/lib/sw-version.js index f2bf471..d6190bb 100644 --- a/tools/graphics-devtool/src/lib/sw-version.js +++ b/tools/graphics-devtool/src/lib/sw-version.js @@ -1 +1 @@ -export const SW_VERSION = '2025-01-09T15:45:17.838Z' // Updated at build time +export const SW_VERSION = '2025-01-15T14:20:24.541Z' // Updated at build time diff --git a/tools/graphics-devtool/src/service-worker.js b/tools/graphics-devtool/src/service-worker.js index 954b332..a598404 100644 --- a/tools/graphics-devtool/src/service-worker.js +++ b/tools/graphics-devtool/src/service-worker.js @@ -1,4 +1,4 @@ -const SW_VERSION = '2025-01-09T15:45:17.838Z' // Updated at build time +const SW_VERSION = '2025-01-15T14:20:24.541Z' // Updated at build time let requestId = 0 const requestMap = new Map()