diff --git a/ts/examples/vite-example/src/App.tsx b/ts/examples/vite-example/src/App.tsx
index 7b1795e90..517259ef4 100644
--- a/ts/examples/vite-example/src/App.tsx
+++ b/ts/examples/vite-example/src/App.tsx
@@ -11,6 +11,7 @@ import WhipExample from './examples/WhipExample';
import DemoExample from './examples/Demo';
import MultipleOutputs from './examples/MultipleOutputs';
import MediaStreamInput from './examples/MediaStreamExample';
+import UploadMp4Example from './examples/UploadMp4Example';
setWasmBundleUrl('/assets/smelter.wasm');
@@ -25,6 +26,7 @@ function App() {
camera: ,
screenCapture: ,
mediaStream: ,
+ uploadMp4: ,
home: ,
demo: ,
};
@@ -49,6 +51,7 @@ function App() {
+
Smelter rendering engine examples
diff --git a/ts/examples/vite-example/src/components/SmelterVideo.tsx b/ts/examples/vite-example/src/components/SmelterVideo.tsx
index d89567dd2..d0d7a214a 100644
--- a/ts/examples/vite-example/src/components/SmelterVideo.tsx
+++ b/ts/examples/vite-example/src/components/SmelterVideo.tsx
@@ -1,5 +1,5 @@
-import React, { useCallback, useEffect, useState } from 'react';
-import Smelter from '@swmansion/smelter-web-wasm';
+import React, { useCallback } from 'react';
+import type Smelter from '@swmansion/smelter-web-wasm';
type VideoProps = React.DetailedHTMLProps<
React.VideoHTMLAttributes,
@@ -7,14 +7,14 @@ type VideoProps = React.DetailedHTMLProps<
>;
type CompositorVideoProps = {
- onVideoCreate?: (smelter: Smelter) => Promise;
- onVideoStarted?: (smelter: Smelter) => Promise;
+ outputId: string;
+ onVideoCreated?: (smelter: Smelter) => Promise;
+ smelter: Smelter;
children: React.ReactElement;
} & VideoProps;
export default function CompositorVideo(props: CompositorVideoProps) {
- const { onVideoCreate, onVideoStarted, children, ...videoProps } = props;
- const [smelter, setSmelter] = useState(undefined);
+ const { outputId, onVideoCreated, children, smelter, ...videoProps } = props;
const videoRef = useCallback(
async (video: HTMLVideoElement | null) => {
@@ -22,15 +22,11 @@ export default function CompositorVideo(props: CompositorVideoProps) {
return;
}
- const smelter = new Smelter({});
-
- await smelter.init();
-
- if (onVideoCreate) {
- await onVideoCreate(smelter);
+ if (onVideoCreated) {
+ await onVideoCreated(smelter);
}
- const { stream } = await smelter.registerOutput('output', children, {
+ const { stream } = await smelter.registerOutput(outputId, children, {
type: 'stream',
video: {
resolution: {
@@ -41,27 +37,13 @@ export default function CompositorVideo(props: CompositorVideoProps) {
audio: true,
});
- await smelter.start();
- setSmelter(smelter);
-
- if (onVideoStarted) {
- await onVideoStarted(smelter);
- }
if (stream) {
video.srcObject = stream;
await video.play();
}
},
- [onVideoCreate, onVideoStarted, videoProps.width, videoProps.height, children]
+ [onVideoCreated, videoProps.width, videoProps.height, smelter, outputId]
);
- useEffect(() => {
- return () => {
- if (smelter) {
- void smelter.terminate();
- }
- };
- }, [smelter]);
-
return ;
}
diff --git a/ts/examples/vite-example/src/examples/MultipleOutputs.tsx b/ts/examples/vite-example/src/examples/MultipleOutputs.tsx
index a831f78c3..a2d05415f 100644
--- a/ts/examples/vite-example/src/examples/MultipleOutputs.tsx
+++ b/ts/examples/vite-example/src/examples/MultipleOutputs.tsx
@@ -1,7 +1,8 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import Smelter from '@swmansion/smelter-web-wasm';
import { InputStream, Rescaler, Text, Tiles, useInputStreams, View } from '@swmansion/smelter';
import NotoSansFont from '../../assets/NotoSans.ttf';
+import CompositorVideo from '../components/SmelterVideo';
const FIRST_MP4_URL =
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4';
@@ -138,52 +139,4 @@ function useSmelter(): Smelter | undefined {
return smelter;
}
-type VideoProps = React.DetailedHTMLProps<
- React.VideoHTMLAttributes,
- HTMLVideoElement
->;
-
-type CompositorVideoProps = {
- outputId: string;
- onVideoCreated?: (smelter: Smelter) => Promise;
- smelter: Smelter;
- children: React.ReactElement;
-} & VideoProps;
-
-function CompositorVideo(props: CompositorVideoProps) {
- const { outputId, onVideoCreated, children, smelter: initialSmelter, ...videoProps } = props;
- const [smelter, _setSmelter] = useState(initialSmelter);
-
- const videoRef = useCallback(
- async (video: HTMLVideoElement | null) => {
- if (!video) {
- return;
- }
-
- if (onVideoCreated) {
- await onVideoCreated(smelter);
- }
-
- const { stream } = await smelter.registerOutput(outputId, children, {
- type: 'stream',
- video: {
- resolution: {
- width: Number(videoProps.width ?? video.width),
- height: Number(videoProps.height ?? video.height),
- },
- },
- audio: true,
- });
-
- if (stream) {
- video.srcObject = stream;
- await video.play();
- }
- },
- [onVideoCreated, videoProps.width, videoProps.height, smelter, outputId]
- );
-
- return ;
-}
-
export default MultipleOutputs;
diff --git a/ts/examples/vite-example/src/examples/UploadMp4Example.tsx b/ts/examples/vite-example/src/examples/UploadMp4Example.tsx
new file mode 100644
index 000000000..5ee27df78
--- /dev/null
+++ b/ts/examples/vite-example/src/examples/UploadMp4Example.tsx
@@ -0,0 +1,99 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import Smelter from '@swmansion/smelter-web-wasm';
+import { InputStream, Rescaler, useInputStreams, View, Text } from '@swmansion/smelter';
+import CompositorVideo from '../components/SmelterVideo';
+import NotoSansFont from '../../assets/NotoSans.ttf';
+
+function UploadMp4Example() {
+ const smelter = useSmelter();
+
+ const onCreate = useCallback(async (smelter: Smelter) => {
+ await smelter.registerFont(NotoSansFont);
+ }, []);
+ const onUpload = async (e: React.ChangeEvent) => {
+ if (!e.target.files) {
+ console.error('No files were uploaded');
+ return;
+ }
+
+ let file = e.target.files[0];
+
+ if (!smelter) {
+ console.error('Smelter has not been initialized yet');
+ return;
+ }
+
+ await smelter.unregisterInput('file');
+ await smelter.registerInput('file', { type: 'mp4', blob: file });
+ };
+
+ if (!smelter) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function Scene() {
+ const inputs = useInputStreams();
+ if (!inputs['file']) {
+ return (
+
+
+ Upload an MP4 file
+
+
+ );
+ }
+ return (
+
+
+
+
+
+ );
+}
+
+function useSmelter(): Smelter | undefined {
+ const [smelter, setSmelter] = useState();
+ useEffect(() => {
+ const smelter = new Smelter();
+
+ let cancel = false;
+ const promise = (async () => {
+ await smelter.init();
+ await smelter.start();
+ if (!cancel) {
+ setSmelter(smelter);
+ }
+ })();
+
+ return () => {
+ cancel = true;
+ void (async () => {
+ await promise.catch(() => {});
+ await smelter.terminate();
+ })();
+ };
+ }, []);
+ return smelter;
+}
+
+export default UploadMp4Example;
diff --git a/ts/smelter-core/src/api/input.ts b/ts/smelter-core/src/api/input.ts
index 1f569e329..fdc01bb5c 100644
--- a/ts/smelter-core/src/api/input.ts
+++ b/ts/smelter-core/src/api/input.ts
@@ -13,6 +13,7 @@ import { _smelterInternals } from '@swmansion/smelter';
*/
export type RegisterInputRequest =
| Api.RegisterInput
+ | { type: 'mp4_blob'; blob: any }
| { type: 'camera' }
| { type: 'screen_capture' }
| { type: 'stream'; stream: any };
@@ -52,6 +53,13 @@ export function intoRegisterInput(input: RegisterInput): RegisterInputRequest {
}
function intoMp4RegisterInput(input: Inputs.RegisterMp4Input): RegisterInputRequest {
+ if (input.blob) {
+ return {
+ type: 'mp4_blob',
+ blob: input.blob,
+ };
+ }
+
return {
type: 'mp4',
url: input.url,
diff --git a/ts/smelter-web-wasm/src/compositor/api.ts b/ts/smelter-web-wasm/src/compositor/api.ts
index ac369138d..8ecc9874c 100644
--- a/ts/smelter-web-wasm/src/compositor/api.ts
+++ b/ts/smelter-web-wasm/src/compositor/api.ts
@@ -52,7 +52,9 @@ export function intoRegisterOutputRequest(request: RegisterOutput): Output.Regis
}
export type RegisterInput =
- | { type: 'mp4'; url: string }
+ | ({ type: 'mp4' } & RegisterMP4Input)
| { type: 'camera' }
| { type: 'screen_capture' }
| { type: 'stream'; stream: MediaStream };
+
+type RegisterMP4Input = { url: string } | { blob: Blob };
diff --git a/ts/smelter-web-wasm/src/mainContext/input.ts b/ts/smelter-web-wasm/src/mainContext/input.ts
index d79294367..72d8be6b8 100644
--- a/ts/smelter-web-wasm/src/mainContext/input.ts
+++ b/ts/smelter-web-wasm/src/mainContext/input.ts
@@ -1,6 +1,6 @@
import type { Input as CoreInput } from '@swmansion/smelter-core';
import type { WorkerMessage } from '../workerApi';
-import { assert } from '../utils';
+import { assert, downloadToArrayBuffer } from '../utils';
import { handleRegisterCameraInput } from './input/camera';
import { handleRegisterScreenCaptureInput } from './input/screenCapture';
import { handleRegisterStreamInput } from './input/stream';
@@ -23,7 +23,11 @@ export async function handleRegisterInputRequest(
): Promise {
if (body.type === 'mp4') {
assert(body.url, 'mp4 URL is required');
- return handleRegisterMp4Input(ctx, inputId, body.url);
+ const arrayBuffer = await downloadToArrayBuffer(body.url);
+ return await handleRegisterMp4Input(ctx, inputId, arrayBuffer);
+ } else if (body.type === 'mp4_blob') {
+ const arrayBuffer = await (body.blob as Blob).arrayBuffer();
+ return await handleRegisterMp4Input(ctx, inputId, arrayBuffer);
} else if (body.type === 'camera') {
return await handleRegisterCameraInput(ctx, inputId);
} else if (body.type === 'screen_capture') {
diff --git a/ts/smelter-web-wasm/src/mainContext/input/mp4.ts b/ts/smelter-web-wasm/src/mainContext/input/mp4.ts
index 2acd1397e..8896bf965 100644
--- a/ts/smelter-web-wasm/src/mainContext/input/mp4.ts
+++ b/ts/smelter-web-wasm/src/mainContext/input/mp4.ts
@@ -21,11 +21,8 @@ export class Mp4Input implements Input {
export async function handleRegisterMp4Input(
ctx: InstanceContext,
inputId: string,
- url: string
+ arrayBuffer: ArrayBuffer
): Promise {
- const response = await fetch(url);
- const arrayBuffer = await response.arrayBuffer();
-
const metadata = await parseMp4(arrayBuffer);
let messagePort;
diff --git a/ts/smelter-web-wasm/src/utils.ts b/ts/smelter-web-wasm/src/utils.ts
index 2a188c851..e1d8db445 100644
--- a/ts/smelter-web-wasm/src/utils.ts
+++ b/ts/smelter-web-wasm/src/utils.ts
@@ -24,3 +24,8 @@ export async function sleep(timeoutMs: number): Promise {
export function framerateToDurationMs(framerate: Framerate): number {
return (1000 * framerate.den) / framerate.num;
}
+
+export async function downloadToArrayBuffer(url: string): Promise {
+ const response = await fetch(url);
+ return await response.arrayBuffer();
+}
diff --git a/ts/smelter/src/types/registerInput.ts b/ts/smelter/src/types/registerInput.ts
index 5ca3cdd47..51ef27a6c 100644
--- a/ts/smelter/src/types/registerInput.ts
+++ b/ts/smelter/src/types/registerInput.ts
@@ -39,6 +39,10 @@ export type RegisterMp4Input = {
* Path to the MP4 file (location on the server where Smelter server is deployed).
*/
serverPath?: string | null;
+ /**
+ * Blob of the MP4 file (available only in smelter-web-wasm).
+ */
+ blob?: any | null;
/**
* (**default=`false`**) If input should be played in the loop. Added in v0.4.0
*/