From 4fb8e3056d966be63e42b76fd553a5633ac47778 Mon Sep 17 00:00:00 2001 From: Casper Date: Fri, 19 May 2023 08:41:05 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + .prettierrc | 7 + LICENSE | 21 ++ README.md | 51 ++++ package-lock.json | 238 ++++++++++++++++++ package.json | 32 +++ src/AudioState.ts | 6 + src/CompositionState.ts | 8 + src/ElementState.ts | 36 +++ src/Preview.ts | 527 ++++++++++++++++++++++++++++++++++++++++ src/PreviewState.ts | 38 +++ src/TextState.ts | 6 + src/VideoState.ts | 6 + src/index.ts | 7 + tsconfig.json | 15 ++ 15 files changed, 1001 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 LICENSE create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/AudioState.ts create mode 100644 src/CompositionState.ts create mode 100644 src/ElementState.ts create mode 100644 src/Preview.ts create mode 100644 src/PreviewState.ts create mode 100644 src/TextState.ts create mode 100644 src/VideoState.ts create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa49873 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.idea \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f4a6845 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 120, + "trailingComma": "all", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..567cb43 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Creatomate + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..36f18b1 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# Creatomate Preview SDK + +The [Preview SDK](https://creatomate.com/javascript-video-sdk) lets you display video and image previews in your web app prior to creating a final MP4, GIF, JPEG or PNG through the API. This is useful when developing a web-based editing or customizer tool, and want to show a real-time rendering in the browser. + +This package is for browser-based apps, and it works with any framework such as React, Angular, Vue, as well as plain Javascript. For creating videos and images using Node.js, refer to the `creatomate` library instead, a different NPM package that [can be found here](https://www.npmjs.com/package/creatomate). + +[Creatomate](https://creatomate.com) is a media generation API for editing and rendering videos and images using code. The platform uses templates and JSON for generating **MP4**, **GIF**, **JPEG** or **PNG** files. All processing is handled by Creatomate's cloud infrastructure, so you do not need to maintain your own servers. + +## Usage + +### Installation + +Install the package using the following command: + +```bash +npm install @creatomate/preview +``` + +You can now load a video template (or [JSON](https://creatomate.com/docs/json/introduction)) as follows: + +```javascript +import { Preview } from '@creatomate/preview'; + +// The following assumes that you have a HTML element like this:
+const containerElement = document.getElementById('container'); + +// Initialize a preview to be spawned within the container +const preview = new Preview(containerElement, 'player', 'YOUR_VIDEO_PLAYER_TOKEN'); + +// Once the SDK is ready, load a template from the project +preview.onReady = async () => { + await preview.loadTemplate('YOUR_TEMPLATE_ID'); + + // You can also load a video from JSON: + // await preview.setSource({ /* Your JSON here */ }); +}; +``` + +Check out the demo code provided below for a more comprehensive example. + +### Demo + +See the Preview SDK in action here: [Video Preview Demo](https://github.com/Creatomate/video-preview-demo) + +## Issues & Comments + +Feel free to contact us if you encounter any issues with the library or Creatomate API at [support@creatomate.com](mailto:support@creatomate.com). + +## License + +The Creatomate Preview SDK is licensed under the MIT license. Please refer to the [LICENSE](https://github.com/Creatomate/creatomate-node/blob/main/LICENSE) for more information. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..df02d09 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,238 @@ +{ + "name": "@creatomate/preview", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@creatomate/preview", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "uuid": "^9.0.0" + }, + "devDependencies": { + "@types/uuid": "^9.0.0", + "prettier": "^2.7.1", + "ts-node": "^10.9.1", + "typescript": "^4.8.4" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.1.tgz", + "integrity": "sha512-DqJociPbZP1lbZ5SQPk4oag6W7AyaGMO6gSfRwq3PWl4PXTwJpRQJhDq4W0kzrg3w6tJ1SwlvGZ5uKFHY13LIg==", + "dev": true, + "peer": true + }, + "node_modules/@types/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..386f625 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "@creatomate/preview", + "version": "1.0.0", + "description": "Render video and image previews in your web app prior to creating a final MP4, GIF, JPEG or PNG through the API.", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "prepack": "npm run build" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/creatomate/creatomate-preview.git" + }, + "keywords": [], + "author": "Creatomate", + "license": "MIT", + "homepage": "https://creatomate.com/javascript-video-sdk", + "devDependencies": { + "@types/uuid": "^9.0.0", + "prettier": "^2.7.1", + "ts-node": "^10.9.1", + "typescript": "^4.8.4" + }, + "dependencies": { + "uuid": "^9.0.0" + } +} diff --git a/src/AudioState.ts b/src/AudioState.ts new file mode 100644 index 0000000..aedaf1c --- /dev/null +++ b/src/AudioState.ts @@ -0,0 +1,6 @@ +export interface AudioState { + /** + * Audio element property. The total length of the media file used in the element. + */ + mediaDuration?: number; +} diff --git a/src/CompositionState.ts b/src/CompositionState.ts new file mode 100644 index 0000000..aa7a0a1 --- /dev/null +++ b/src/CompositionState.ts @@ -0,0 +1,8 @@ +import { ElementState } from './ElementState'; + +export interface CompositionState { + /** + * Composition element property. The elements in the composition. + */ + elements?: ElementState[]; +} diff --git a/src/ElementState.ts b/src/ElementState.ts new file mode 100644 index 0000000..e4af5f9 --- /dev/null +++ b/src/ElementState.ts @@ -0,0 +1,36 @@ +import { CompositionState } from './CompositionState'; +import { TextState } from './TextState'; +import { VideoState } from './VideoState'; +import { AudioState } from './AudioState'; + +export interface ElementState extends CompositionState, TextState, VideoState, AudioState { + /** + * This element's track number. + */ + track: number; + + /** + * This element's appearance time in relation to its composition. + */ + localTime: number; + + /** + * This element's appearance time in relation to the entire video. + */ + globalTime: number; + + /** + * This element's duration in seconds. + */ + duration: number; + + /** + * Exit transition duration in seconds, i.e. how long the next element overlaps it. + */ + exitDuration: number; + + /** + * The source JSON of the element without the 'elements' property. + */ + source: Record; +} diff --git a/src/Preview.ts b/src/Preview.ts new file mode 100644 index 0000000..30b7366 --- /dev/null +++ b/src/Preview.ts @@ -0,0 +1,527 @@ +import { v4 as uuid } from 'uuid'; +import { PreviewState } from './PreviewState'; +import { ElementState } from './ElementState'; + +export class Preview { + /** + * Called when the plugin is ready for use. Do not use any of the functions of this instance prior to this event. + * + * @see ready + */ + onReady?: () => void; + + /** + * Called when the preview is entering the 'loading' state. This occurs when an asset is downloading or buffering. + */ + onLoad?: () => void; + + /** + * Called when the preview is exiting the 'loading' state. + */ + onLoadComplete?: () => void; + + /** + * Called when the video starts playing, either by calling 'play()' or the user pressing the play button in player mode. + * + * @see play() + */ + onPlay?: () => void; + + /** + * Called when the video stops playing, either by calling 'pause()', the user pressing the pause button in player mode, + * or the preview pauses playback temporarily while downloading or buffering. + * + * @see pause() + */ + onPause?: () => void; + + /** + * Called when the playback time of the video has changed. + * + * @param time The current playback time in seconds. + * @see setTime() + */ + onTimeChange?: (time: number) => void; + + /** + * Called when the mouse tool has changed in interactive mode. + * + * @param tool Any of the available mouse tools; default, pen, text, ellipse, or rectangle. + * @see setTool() + */ + onToolChange?: (tool: 'default' | 'pen' | 'text' | 'ellipse' | 'rectangle') => void; + + /** + * Called when the active elements have changed in interactive mode. + * + * @param elementIds The IDs of the elements that are currently selected. + * @see setActiveElements() + */ + onActiveElementsChange?: (elementIds: string[]) => void; + + /** + * Called when the state of the preview has changed. + * + * @param state The current state of the preview. + */ + onStateChange?: (state: PreviewState) => void; + + /** + * The current mode as set in the constructor or 'setMode()'. + * + * @see constructor + * @see setMode() + */ + mode: 'player' | 'interactive'; + + /** + * Whether the plugin is ready for use. Do not use any of the functions of this instance prior to the plugin is ready. + * + * @see onReady() + */ + ready = false; + + /** + * The current state of the preview. Do not modify, this is a readonly property. + * To change the state, use 'loadTemplate()', 'setSource()', 'setModifications()', or 'applyModifications()'. + * Subscribe to the 'onStateChange()' event to be notified when the state changes. + * + * @see loadTemplate() + * @see setSource() + * @see onStateChange() + */ + state?: PreviewState; + + private readonly _iframe: HTMLIFrameElement; + + private _pendingPromises: Record void; reject: (reason: any) => void }> = {}; + + /** + * Sets up the Creatomate Web SDK plugin. You can choose between 'player' and 'interactive' modes. + * - The player mode offers a static video player with a play button and time scrubber. + * - In interactive mode, users can change the content by dragging and dropping, just like in Creatomate's template editor. + * + * @param element The HTML DIV element to use as a container. The preview automatically adjusts the size of this element. + * @param mode Choose 'player' or 'interactive'. When in doubt, choose 'player'. + * @param publicToken Your project's public token. You can find your token in your Creatomate dashboard under project settings. + */ + constructor(public element: HTMLDivElement, mode: 'player' | 'interactive', publicToken: string) { + this.mode = mode; + + const iframe = document.createElement('iframe'); + iframe.setAttribute('width', '100%'); + iframe.setAttribute('height', '100%'); + iframe.setAttribute('scrolling', 'no'); + iframe.setAttribute('allow', 'autoplay'); + iframe.setAttribute('src', `https://creatomate.com/embed?token=${publicToken}`); + iframe.style.border = 'none'; + iframe.style.display = 'none'; + + element.innerHTML = ''; + element.style.overflow = 'hidden'; + element.append(iframe); + + window.addEventListener('message', this._handleMessage); + + this._iframe = iframe; + } + + /** + * Disposes the resources of this plugin. You can call this after you have finished using it. + */ + dispose() { + window.removeEventListener('message', this._handleMessage); + + this._iframe.parentNode?.removeChild(this._iframe); + this._iframe.setAttribute('src', ''); + + this._pendingPromises = {}; + } + + /** + * Sets the mode. See the constructor for more information about these modes. + * + * @param mode 'player' or 'interactive' mode. + */ + async setMode(mode: 'player' | 'interactive'): Promise { + await this._sendCommand({ message: 'setMode', mode }).catch((error) => { + throw new Error(`Failed to set mode: ${error.message}`); + }); + } + + /** + * Loads a template from your project. + * Make sure the template is located in the project from which you are using the public token. + * + * @param templateId The ID of the template. + */ + async loadTemplate(templateId: string): Promise { + await this._sendCommand({ message: 'setTemplate', templateId }).catch((error) => { + throw new Error(`Failed to load template: ${error.message}`); + }); + + // Show iframe + this._iframe.style.display = ''; + } + + /** + * Sets the JSON source of the video or image that is being displayed. + * + * @param source The source of the video or image. + * @param createUndoPoint Set to 'true' if you wish to add an undo point to be used later with 'undo()' and 'redo()'. + * @see https://creatomate.com/docs/json/introduction + * @see undo() + * @see redo() + */ + async setSource(source: Record, createUndoPoint = false): Promise { + await this._sendCommand({ message: 'setSource', source, createUndoPoint }).catch((error) => { + throw new Error(`Failed to set source: ${error.message}`); + }); + + // Show iframe + this._iframe.style.display = ''; + } + + /** + * Gets the source JSON of an element, or the full source of the video/image if no element is provided. + * + * @param state Optional. The state from which to extract the source JSON. Defaults to 'this.state'. + * @return The source JSON. + * @see https://creatomate.com/docs/json/introduction + */ + getSource( + state: { source: Record; elements?: ElementState[] } | undefined = this.state, + ): Record { + if (!state) { + return {}; + } else if (state.elements) { + return { + ...state.source, + elements: state.elements.map((element) => this.getSource(element)), + }; + } else { + return state.source; + } + } + + /** + * Gets all nested elements of an element recursively, or all elements of the video/image if no element is provided. + * + * @param state Optional. The state from which to retrieve the elements. Defaults to 'this.state'. + * @return The elements in an array. + */ + getElements( + state: { source: Record; elements?: ElementState[] } | undefined = this.state, + ): ElementState[] { + const elements = []; + if (state) { + if (state.source.type) { + elements.push(state as ElementState); + } + + if (Array.isArray(state.elements)) { + for (const nestedElement of state.elements) { + elements.push(...this.getElements(nestedElement)); + } + } + } + return elements; + } + + /** + * Finds an element by a predicate recursively. + * + * @param predicate Predicate function that is called for each element until 'true' is returned. + * @param state Optional. The state to scan for an element. Defaults to 'this.state'. + * @return The state of the element found, or 'undefined' when the predicate returned 'false' for all elements. + */ + findElement( + predicate: (element: ElementState) => boolean, + state: { source: Record; elements?: ElementState[] } | undefined = this.state, + ): ElementState | undefined { + if (state?.elements) { + for (const element of state.elements) { + if (predicate(element)) { + return element; + } + + if (element.elements) { + const foundNestedElement = this.findElement(predicate, element); + if (foundNestedElement) { + return foundNestedElement; + } + } + } + } + return undefined; + } + + /** + * Sets the modifications that are applied to the video or image source. + * Unlike 'applyModifications()', this function does not mutate the source set by 'loadTemplate()' or 'setSource()'. + * + * How do I know when to use 'setModifications' or 'applyModifications'? + * - If you use the 'player' mode and wish to replace the content of dynamic elements, 'setModifications()' is probably what you need. + * - If you use the 'interactive' mode and want to make a definitive change to any element, you'll probably want to use 'applyModifications()' instead. + * + * @param modifications A modifications object. + * @see https://creatomate.com/docs/api/rest-api/the-modifications-object + * @see applyModifications() + * @see loadTemplate() + * @see setSource() + */ + async setModifications(modifications: Record): Promise { + return await this._sendCommand({ message: 'setModifications', modifications }).catch((error) => { + throw new Error(`Failed to set modifications: ${error.message}`); + }); + } + + /** + * Applies modifications to the source. An undo point is created automatically. + * The difference between this and 'setModifications()' is that this function modifies the source JSON. + * + * @param modifications A modifications object. + * @see https://creatomate.com/docs/api/rest-api/the-modifications-object + * @see setModifications() + * @see undo() + * @see redo() + */ + async applyModifications(modifications: Record): Promise { + return this._sendCommand({ message: 'applyModifications', modifications }).catch((error) => { + throw new Error(`Failed to apply modifications: ${error.message}`); + }); + } + + /** + * Reverts the last changes made by 'setSource()', 'applyModifications()', or the user when in interactive mode. + * + * @see setSource() + * @see applyModifications() + */ + async undo(): Promise { + return this._sendCommand({ message: 'undo' }).catch((error) => { + throw new Error(`Failed to undo: ${error.message}`); + }); + } + + /** + * Reapplies the changes from the redo stack. This function won't have any effect if 'undo()' has not been called previously. + * + * @see undo() + */ + async redo(): Promise { + return this._sendCommand({ message: 'redo' }).catch((error) => { + throw new Error(`Failed to redo: ${error.message}`); + }); + } + + /** + * Sets the current mouse tool. + * + * @param tool Any of the available mouse tools; default, pen, text, ellipse, or rectangle. + * @see onToolChange() + */ + async setTool(tool: 'default' | 'pen' | 'text' | 'ellipse' | 'rectangle') { + await this._sendCommand({ message: 'setTool', tool }).catch((error) => { + throw new Error(`Failed to set tool: ${error.message}`); + }); + } + + /** + * Sets the currently selected elements when in interactive mode. + * + * @param elementIds The IDs of the elements to select. + * @see onActiveElementsChange() + */ + async setActiveElements(elementIds: string[]): Promise { + await this._sendCommand({ message: 'setActiveElements', elementIds }).catch((error) => { + throw new Error(`Failed to set active elements: ${error.message}`); + }); + } + + /** + * Sets the zoom state in interactive mode. + * NOTE: This is an experimental setting and is likely to change in the near future. + * + * Zoom mode can be any of these values: + * - 'free': Allows the user to freely pan and zoom the canvas. The default option. + * - 'auto': The canvas is automatically scaled according to the viewport size. + * - 'fixed': Set the canvas to a fixed scale as provided by the 'scale' parameter. + * - 'centered': Keeps the canvas in the center when zoomed out. + * + * @param mode Zoom mode. + * @param scale Optional zoom scale (1.0 = 100%). + */ + async setZoom(mode: 'free' | 'auto' | 'fixed' | 'centered', scale?: number) { + await this._sendCommand({ message: 'setZoom', mode, scale }).catch((error) => { + throw new Error(`Failed to set the zoom state: ${error.message}`); + }); + } + + /** + * Seeks to the provided playback time. + * + * @param time Playback time in seconds. + * @see onTimeChange() + */ + async setTime(time: number): Promise { + return this._sendCommand({ message: 'setTime', time }).catch((error) => { + throw new Error(`Failed to set time: ${error.message}`); + }); + } + + /** + * Starts playing the video. + * + * @see onPlay() + */ + async play(): Promise { + return this._sendCommand({ message: 'play' }).catch((error) => { + throw new Error(`Failed to play: ${error.message}`); + }); + } + + /** + * Pauses the video. + * + * @see onPause() + */ + async pause(): Promise { + return this._sendCommand({ message: 'pause' }).catch((error) => { + throw new Error(`Failed to pause: ${error.message}`); + }); + } + + /** + * Ensures that an asset can be used immediately as a source for a video, image, or audio element by caching it. + * As a result, the file is immediately available without waiting for the upload to complete. + * + * @param url The URL of the file. This URL won't be requested because the Blob should provide the file content already. + * @param blob The content of the file. Make sure that the file is available at the URL eventually, + * as there is no guarantee that it will remain cached. + * @see https://developer.mozilla.org/en-US/docs/Web/API/Blob + * @see https://developer.mozilla.org/en-US/docs/Web/API/Cache + */ + cacheAsset(url: string, blob: Blob): Promise { + return this._sendCommand({ message: 'cacheAsset', url }, { blob }).catch((error) => { + throw new Error(`Failed to cache asset: ${error.message}`); + }); + } + + /** + * Sets a list of RegExp rules used to determine whether a video asset should be fully cached on the client's device. + * This is especially useful for large video files that take a long time to download. + * These rules do not apply to files hosted by the Creatomate CDN, because those are always cached. + * NOTE: This is an experimental setting and is likely to change in the near future. + * + * @param rules A list of regular expressions matched against every video URL. + * @example + * // Disable caching of video files for URLs beginning with https://www.example.com/ + * setCacheBypassRules([ /^https:\/\/www\.example\.com\// ]); + */ + setCacheBypassRules(rules: RegExp[]) { + const serializedRules = rules.map((rule) => rule.source); + return this._sendCommand({ message: 'setCacheBypassRules', rules: serializedRules }).catch((error) => { + throw new Error(`Failed to set cache bypass rules: ${error.message}`); + }); + } + + private _sendCommand(message: Record, payload?: Record): Promise { + if (!this.ready) { + throw new Error('The SDK is not yet ready. Please wait for the onReady event before calling any methods.'); + } + + const id = uuid(); + this._iframe.contentWindow?.postMessage({ id, ...JSON.parse(JSON.stringify(message)), ...payload }, '*'); + + // Create pending promise + return new Promise((resolve, reject) => (this._pendingPromises[id] = { resolve, reject })); + } + + // Defined as arrow function to make it bound to this instance when used with window.addEventListener above. + private _handleMessage = (e: MessageEvent) => { + if (!e.data || typeof e.data !== 'object') { + return; + } + + const { id, message, error, ...args } = e.data; + + if (id) { + // Resolve pending promise + const pendingPromise = this._pendingPromises[id]; + if (pendingPromise) { + if (error) { + pendingPromise.reject(new Error(error)); + } else { + pendingPromise.resolve(args); + } + + // Clean up + delete this._pendingPromises[id]; + } + } else { + switch (message) { + case 'onReady': + // The component is ready to use + this.ready = true; + + // Set the mode as provided in the constructor + this.setMode(this.mode).then(); + + if (this.onReady) { + this.onReady(); + } + break; + + case 'onLoad': + if (this.onLoad) { + this.onLoad(); + } + break; + + case 'onLoadComplete': + if (this.onLoadComplete) { + this.onLoadComplete(); + } + break; + + case 'onPlay': + if (this.onPlay) { + this.onPlay(); + } + break; + + case 'onPause': + if (this.onPause) { + this.onPause(); + } + break; + + case 'onTimeChange': + if (this.onTimeChange) { + this.onTimeChange(args.time); + } + break; + + case 'onToolChange': + if (this.onToolChange) { + this.onToolChange(args.tool); + } + break; + + case 'onActiveElementsChange': + if (this.onActiveElementsChange) { + this.onActiveElementsChange(args.elementIds); + } + break; + + case 'onStateChange': + this.state = args.state; + if (this.onStateChange) { + this.onStateChange(args.state); + } + break; + } + } + }; +} diff --git a/src/PreviewState.ts b/src/PreviewState.ts new file mode 100644 index 0000000..cfadcb3 --- /dev/null +++ b/src/PreviewState.ts @@ -0,0 +1,38 @@ +import { ElementState } from './ElementState'; + +export interface PreviewState { + /** + * Width of the video in pixels. + */ + width: number; + + /** + * Height of the video in pixels. + */ + height: number; + + /** + * Duration of the video in seconds. + */ + duration: number; + + /** + * When the preview state can be reversed, it is 'true'. See the undo and redo functions. + */ + undo: boolean; + + /** + * When the preview state can be reapplied, it is 'true'. See the undo and redo functions. + */ + redo: boolean; + + /** + * The source JSON of the video/image without the 'elements' property. + */ + source: Record; + + /** + * The elements in this video/image + */ + elements: ElementState[]; +} diff --git a/src/TextState.ts b/src/TextState.ts new file mode 100644 index 0000000..e0a416e --- /dev/null +++ b/src/TextState.ts @@ -0,0 +1,6 @@ +export interface TextState { + /** + * Text element property. The fixed or auto-calculated font size of the text element. + */ + fontSize?: number; +} diff --git a/src/VideoState.ts b/src/VideoState.ts new file mode 100644 index 0000000..d6fb43f --- /dev/null +++ b/src/VideoState.ts @@ -0,0 +1,6 @@ +export interface VideoState { + /** + * Video element property. The total length of the media file used in the element. + */ + mediaDuration?: number; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6182331 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,7 @@ +export { Preview } from './Preview'; +export { PreviewState } from './PreviewState'; +export { ElementState } from './ElementState'; +export { TextState } from './TextState'; +export { VideoState } from './VideoState'; +export { AudioState } from './AudioState'; +export { CompositionState } from './CompositionState'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d085069 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "target": "es6", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "outDir": "./dist" + }, + "include": [ + "src/*" + ] +}