@remotion/studio 4.0.422 → 4.0.423

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/api/save-render-output.d.ts +7 -0
  2. package/dist/api/save-render-output.js +45 -0
  3. package/dist/components/RenderModal/ClientRenderProgress.js +3 -0
  4. package/dist/components/RenderModal/RenderStatusModal.js +4 -0
  5. package/dist/components/RenderModal/ServerRenderModal.js +1 -1
  6. package/dist/components/RenderModal/WebRenderModal.js +7 -2
  7. package/dist/components/RenderModal/WebRenderModalBasic.js +1 -1
  8. package/dist/components/RenderQueue/ClientRenderQueueProcessor.d.ts +1 -0
  9. package/dist/components/RenderQueue/ClientRenderQueueProcessor.js +34 -4
  10. package/dist/components/RenderQueue/RenderQueueDownloadItem.d.ts +5 -0
  11. package/dist/components/RenderQueue/RenderQueueDownloadItem.js +35 -0
  12. package/dist/components/RenderQueue/RenderQueueItem.js +48 -16
  13. package/dist/components/RenderQueue/RenderQueueItemStatus.js +3 -0
  14. package/dist/components/RenderQueue/RenderQueueOpenInFolder.d.ts +2 -2
  15. package/dist/components/RenderQueue/RenderQueueOutputName.js +1 -8
  16. package/dist/components/RenderQueue/RenderQueueRemoveItem.js +4 -2
  17. package/dist/components/RenderQueue/RenderQueueSavingMessage.d.ts +2 -0
  18. package/dist/components/RenderQueue/RenderQueueSavingMessage.js +13 -0
  19. package/dist/components/RenderQueue/client-side-render-types.d.ts +11 -8
  20. package/dist/components/RenderQueue/client-side-render-types.js +5 -0
  21. package/dist/components/RenderQueue/context.d.ts +6 -3
  22. package/dist/components/RenderQueue/context.js +37 -6
  23. package/dist/components/RenderQueue/index.js +1 -14
  24. package/dist/esm/{chunk-4153e552.js → chunk-112w480k.js} +4686 -3752
  25. package/dist/esm/internals.mjs +4686 -3752
  26. package/dist/esm/previewEntry.mjs +4695 -3761
  27. package/dist/esm/renderEntry.mjs +1 -1
  28. package/dist/helpers/client-id.js +4 -1
  29. package/dist/helpers/retry-payload.d.ts +2 -2
  30. package/package.json +10 -10
@@ -0,0 +1,7 @@
1
+ import type { CompletedClientRender } from '@remotion/studio-shared';
2
+ export declare const saveOutputFile: ({ blob, filePath, }: {
3
+ blob: Blob;
4
+ filePath: string;
5
+ }) => Promise<void>;
6
+ export declare const registerClientRender: (render: CompletedClientRender) => Promise<void>;
7
+ export declare const unregisterClientRender: (id: string) => Promise<void>;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.unregisterClientRender = exports.registerClientRender = exports.saveOutputFile = void 0;
4
+ const throwIfNotOk = async (response) => {
5
+ if (!response.ok) {
6
+ try {
7
+ const jsonResponse = await response.json();
8
+ throw new Error(jsonResponse.error);
9
+ }
10
+ catch (parseError) {
11
+ if (parseError instanceof Error && parseError.message) {
12
+ throw parseError;
13
+ }
14
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
15
+ }
16
+ }
17
+ };
18
+ const saveOutputFile = async ({ blob, filePath, }) => {
19
+ const url = new URL('/api/upload-output', window.location.origin);
20
+ url.search = new URLSearchParams({ filePath }).toString();
21
+ const response = await fetch(url, {
22
+ method: 'POST',
23
+ body: blob,
24
+ });
25
+ await throwIfNotOk(response);
26
+ };
27
+ exports.saveOutputFile = saveOutputFile;
28
+ const registerClientRender = async (render) => {
29
+ const response = await fetch('/api/register-client-render', {
30
+ method: 'POST',
31
+ headers: { 'Content-Type': 'application/json' },
32
+ body: JSON.stringify(render),
33
+ });
34
+ await throwIfNotOk(response);
35
+ };
36
+ exports.registerClientRender = registerClientRender;
37
+ const unregisterClientRender = async (id) => {
38
+ const response = await fetch('/api/unregister-client-render', {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({ id }),
42
+ });
43
+ await throwIfNotOk(response);
44
+ };
45
+ exports.unregisterClientRender = unregisterClientRender;
@@ -50,6 +50,9 @@ const ClientRenderProgress = ({ job }) => {
50
50
  if (job.status === 'done') {
51
51
  return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(layout_1.Spacing, { y: 0.5 }), (0, jsx_runtime_1.jsx)(DoneStatus, { job: job }), (0, jsx_runtime_1.jsx)(layout_1.Spacing, { y: 1 })] }));
52
52
  }
53
+ if (job.status === 'saving') {
54
+ return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(layout_1.Spacing, { y: 0.5 }), (0, jsx_runtime_1.jsx)("div", { style: label, children: "Saving to out/..." }), (0, jsx_runtime_1.jsx)(layout_1.Spacing, { y: 1 })] }));
55
+ }
53
56
  const { renderedFrames, encodedFrames, totalFrames } = job.progress;
54
57
  return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(layout_1.Spacing, { y: 0.5 }), (0, jsx_runtime_1.jsx)(RenderingProgress, { renderedFrames: renderedFrames, totalFrames: totalFrames }), job.type === 'client-video' && ((0, jsx_runtime_1.jsx)(EncodingProgress, { encodedFrames: encodedFrames, totalFrames: totalFrames })), (0, jsx_runtime_1.jsx)(layout_1.Spacing, { y: 1 })] }));
55
58
  };
@@ -11,6 +11,7 @@ const ModalContainer_1 = require("../ModalContainer");
11
11
  const ModalHeader_1 = require("../ModalHeader");
12
12
  const NotificationCenter_1 = require("../Notifications/NotificationCenter");
13
13
  const actions_1 = require("../RenderQueue/actions");
14
+ const client_side_render_types_1 = require("../RenderQueue/client-side-render-types");
14
15
  const context_1 = require("../RenderQueue/context");
15
16
  const layout_1 = require("../layout");
16
17
  const ClientRenderProgress_1 = require("./ClientRenderProgress");
@@ -51,6 +52,9 @@ const RenderStatusModal = ({ jobId, }) => {
51
52
  }, [setSelectedModal]);
52
53
  const onRetry = (0, react_1.useCallback)(() => {
53
54
  if (isClientJob) {
55
+ if ((0, client_side_render_types_1.isRestoredClientJob)(job)) {
56
+ return;
57
+ }
54
58
  const retryPayload = (0, retry_payload_1.makeClientRetryPayload)(job);
55
59
  setSelectedModal(retryPayload);
56
60
  }
@@ -103,7 +103,7 @@ const RenderModal = ({ initialFrame, initialVideoImageFormat, initialStillImageF
103
103
  : initialStillImageFormat,
104
104
  type: 'asset',
105
105
  compositionDefaultOutName: resolvedComposition.defaultOutName,
106
- clientSideRender: false,
106
+ outputLocation: renderDefaults.outputLocation,
107
107
  });
108
108
  });
109
109
  const [videoCodecForAudioTab, setVideoCodecForAudioTab] = (0, react_1.useState)(() => initialVideoCodecForAudioTab);
@@ -148,10 +148,11 @@ const WebRenderModal = ({ initialFrame, defaultProps, inFrameMark, outFrameMark,
148
148
  return Math.max(0, Math.min(finalEndFrame, startFrame));
149
149
  }, [finalEndFrame, startFrame]);
150
150
  const [initialOutNameState] = (0, react_1.useState)(() => {
151
+ var _a, _b;
151
152
  if (initialDefaultOutName) {
152
153
  return initialDefaultOutName;
153
154
  }
154
- return (0, studio_shared_1.getDefaultOutLocation)({
155
+ const defaultOut = (0, studio_shared_1.getDefaultOutLocation)({
155
156
  compositionName: resolvedComposition.id,
156
157
  defaultExtension: renderMode === 'still'
157
158
  ? imageFormat
@@ -160,8 +161,12 @@ const WebRenderModal = ({ initialFrame, defaultProps, inFrameMark, outFrameMark,
160
161
  : imageFormat,
161
162
  type: 'asset',
162
163
  compositionDefaultOutName: resolvedComposition.defaultOutName,
163
- clientSideRender: true,
164
+ outputLocation: (_b = (_a = window.remotion_renderDefaults) === null || _a === void 0 ? void 0 : _a.outputLocation) !== null && _b !== void 0 ? _b : null,
164
165
  });
166
+ if (window.remotion_isReadOnlyStudio) {
167
+ return defaultOut.replace(/^out\//, '');
168
+ }
169
+ return defaultOut;
165
170
  });
166
171
  const [outName, setOutName] = (0, react_1.useState)(() => initialOutNameState);
167
172
  const setStillFormat = (0, react_1.useCallback)((format) => {
@@ -101,6 +101,6 @@ const WebRenderModalBasic = ({ renderMode, resolvedComposition, imageFormat, set
101
101
  value: c,
102
102
  }));
103
103
  }, [encodableVideoCodecs, effectiveVideoCodec, setCodec, codecLabels]);
104
- return ((0, jsx_runtime_1.jsxs)("div", { style: tabContainer, children: [renderMode === 'still' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { style: layout_2.optionRow, children: [(0, jsx_runtime_1.jsx)("div", { style: layout_2.label, children: "Format" }), (0, jsx_runtime_1.jsx)("div", { style: layout_2.rightRow, children: (0, jsx_runtime_1.jsx)(SegmentedControl_1.SegmentedControl, { items: imageFormatOptions, needsWrapping: true }) })] }), resolvedComposition.durationInFrames > 1 ? ((0, jsx_runtime_1.jsxs)("div", { style: layout_2.optionRow, children: [(0, jsx_runtime_1.jsx)("div", { style: layout_2.label, children: "Frame" }), (0, jsx_runtime_1.jsx)("div", { style: layout_2.rightRow, children: (0, jsx_runtime_1.jsx)(RemInput_1.RightAlignInput, { children: (0, jsx_runtime_1.jsx)(InputDragger_1.InputDragger, { value: frame, onTextChange: onFrameChanged, placeholder: `0-${resolvedComposition.durationInFrames - 1}`, onValueChange: onFrameSetDirectly, name: "frame", step: 1, min: 0, status: "ok", max: resolvedComposition.durationInFrames - 1, rightAlign: true }) }) })] })) : null] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { style: layout_2.optionRow, children: [(0, jsx_runtime_1.jsx)("div", { style: layout_2.label, children: "Container" }), (0, jsx_runtime_1.jsx)("div", { style: layout_2.rightRow, children: (0, jsx_runtime_1.jsx)(ComboBox_1.Combobox, { values: containerOptions, selectedId: container, title: "Container" }) })] }), (0, jsx_runtime_1.jsxs)("div", { style: layout_2.optionRow, children: [(0, jsx_runtime_1.jsxs)("div", { style: layout_2.label, children: ["Codec", (0, jsx_runtime_1.jsx)(layout_1.Spacing, { x: 0.5 }), (0, jsx_runtime_1.jsx)(OptionExplainerBubble_1.OptionExplainerBubble, { id: "videoCodecOption" })] }), (0, jsx_runtime_1.jsx)("div", { style: layout_2.rightRow, children: (0, jsx_runtime_1.jsx)(ComboBox_1.Combobox, { values: codecOptions, selectedId: effectiveVideoCodec, title: "Codec" }) })] }), (0, jsx_runtime_1.jsx)(FrameRangeSetting_1.FrameRangeSetting, { durationInFrames: resolvedComposition.durationInFrames, startFrame: startFrame !== null && startFrame !== void 0 ? startFrame : 0, endFrame: endFrame !== null && endFrame !== void 0 ? endFrame : resolvedComposition.durationInFrames - 1, setStartFrame: setStartFrame, setEndFrame: setEndFrame })] })), (0, jsx_runtime_1.jsx)(RenderModalOutputName_1.RenderModalOutputName, { existence: false, inputStyle: layout_2.input, outName: outName, onValueChange: onOutNameChange, validationMessage: validationMessage, label: "Download name" }), (0, jsx_runtime_1.jsxs)("div", { style: layout_2.optionRow, children: [(0, jsx_runtime_1.jsxs)("div", { style: layout_2.label, children: ["Log Level ", (0, jsx_runtime_1.jsx)(layout_1.Spacing, { x: 0.5 }), (0, jsx_runtime_1.jsx)(OptionExplainerBubble_1.OptionExplainerBubble, { id: "logLevelOption" })] }), (0, jsx_runtime_1.jsx)("div", { style: layout_2.rightRow, children: (0, jsx_runtime_1.jsx)(ComboBox_1.Combobox, { values: logLevelOptions, selectedId: logLevel, title: "Log Level" }) })] })] }));
104
+ return ((0, jsx_runtime_1.jsxs)("div", { style: tabContainer, children: [renderMode === 'still' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { style: layout_2.optionRow, children: [(0, jsx_runtime_1.jsx)("div", { style: layout_2.label, children: "Format" }), (0, jsx_runtime_1.jsx)("div", { style: layout_2.rightRow, children: (0, jsx_runtime_1.jsx)(SegmentedControl_1.SegmentedControl, { items: imageFormatOptions, needsWrapping: true }) })] }), resolvedComposition.durationInFrames > 1 ? ((0, jsx_runtime_1.jsxs)("div", { style: layout_2.optionRow, children: [(0, jsx_runtime_1.jsx)("div", { style: layout_2.label, children: "Frame" }), (0, jsx_runtime_1.jsx)("div", { style: layout_2.rightRow, children: (0, jsx_runtime_1.jsx)(RemInput_1.RightAlignInput, { children: (0, jsx_runtime_1.jsx)(InputDragger_1.InputDragger, { value: frame, onTextChange: onFrameChanged, placeholder: `0-${resolvedComposition.durationInFrames - 1}`, onValueChange: onFrameSetDirectly, name: "frame", step: 1, min: 0, status: "ok", max: resolvedComposition.durationInFrames - 1, rightAlign: true }) }) })] })) : null] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { style: layout_2.optionRow, children: [(0, jsx_runtime_1.jsx)("div", { style: layout_2.label, children: "Container" }), (0, jsx_runtime_1.jsx)("div", { style: layout_2.rightRow, children: (0, jsx_runtime_1.jsx)(ComboBox_1.Combobox, { values: containerOptions, selectedId: container, title: "Container" }) })] }), (0, jsx_runtime_1.jsxs)("div", { style: layout_2.optionRow, children: [(0, jsx_runtime_1.jsxs)("div", { style: layout_2.label, children: ["Codec", (0, jsx_runtime_1.jsx)(layout_1.Spacing, { x: 0.5 }), (0, jsx_runtime_1.jsx)(OptionExplainerBubble_1.OptionExplainerBubble, { id: "videoCodecOption" })] }), (0, jsx_runtime_1.jsx)("div", { style: layout_2.rightRow, children: (0, jsx_runtime_1.jsx)(ComboBox_1.Combobox, { values: codecOptions, selectedId: effectiveVideoCodec, title: "Codec" }) })] }), (0, jsx_runtime_1.jsx)(FrameRangeSetting_1.FrameRangeSetting, { durationInFrames: resolvedComposition.durationInFrames, startFrame: startFrame !== null && startFrame !== void 0 ? startFrame : 0, endFrame: endFrame !== null && endFrame !== void 0 ? endFrame : resolvedComposition.durationInFrames - 1, setStartFrame: setStartFrame, setEndFrame: setEndFrame })] })), (0, jsx_runtime_1.jsx)(RenderModalOutputName_1.RenderModalOutputName, { existence: false, inputStyle: layout_2.input, outName: outName, onValueChange: onOutNameChange, validationMessage: validationMessage, label: window.remotion_isReadOnlyStudio ? 'Download name' : 'Output name' }), (0, jsx_runtime_1.jsxs)("div", { style: layout_2.optionRow, children: [(0, jsx_runtime_1.jsxs)("div", { style: layout_2.label, children: ["Log Level ", (0, jsx_runtime_1.jsx)(layout_1.Spacing, { x: 0.5 }), (0, jsx_runtime_1.jsx)(OptionExplainerBubble_1.OptionExplainerBubble, { id: "logLevelOption" })] }), (0, jsx_runtime_1.jsx)("div", { style: layout_2.rightRow, children: (0, jsx_runtime_1.jsx)(ComboBox_1.Combobox, { values: logLevelOptions, selectedId: logLevel, title: "Log Level" }) })] })] }));
105
105
  };
106
106
  exports.WebRenderModalBasic = WebRenderModalBasic;
@@ -1 +1,2 @@
1
+ export declare const downloadBlob: (blob: Blob, filename: string) => void;
1
2
  export declare const ClientRenderQueueProcessor: React.FC;
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ClientRenderQueueProcessor = void 0;
3
+ exports.ClientRenderQueueProcessor = exports.downloadBlob = void 0;
4
4
  const web_renderer_1 = require("@remotion/web-renderer");
5
5
  const react_1 = require("react");
6
+ const save_render_output_1 = require("../../api/save-render-output");
6
7
  const context_1 = require("./context");
7
8
  const downloadBlob = (blob, filename) => {
8
9
  const url = URL.createObjectURL(blob);
@@ -15,8 +16,9 @@ const downloadBlob = (blob, filename) => {
15
16
  a.click();
16
17
  URL.revokeObjectURL(url);
17
18
  };
19
+ exports.downloadBlob = downloadBlob;
18
20
  const ClientRenderQueueProcessor = () => {
19
- const { getAbortController, getCompositionForJob, updateClientJobProgress, markClientJobDone, markClientJobFailed, markClientJobCancelled, setProcessJobCallback, } = (0, react_1.useContext)(context_1.RenderQueueContext);
21
+ const { getAbortController, getCompositionForJob, updateClientJobProgress, markClientJobSaving, markClientJobDone, markClientJobFailed, markClientJobCancelled, setProcessJobCallback, } = (0, react_1.useContext)(context_1.RenderQueueContext);
20
22
  const processStillJob = (0, react_1.useCallback)(async (job, signal) => {
21
23
  var _a, _b;
22
24
  const compositionRef = getCompositionForJob(job.id);
@@ -114,13 +116,40 @@ const ClientRenderQueueProcessor = () => {
114
116
  throw new Error(`Unknown job type`);
115
117
  }
116
118
  const blob = await result.getBlob();
117
- downloadBlob(blob, job.outName);
118
119
  const metadata = {
119
120
  width: result.width,
120
121
  height: result.height,
121
122
  sizeInBytes: blob.size,
122
123
  };
123
- markClientJobDone(job.id, result.getBlob, metadata);
124
+ markClientJobSaving(job.id);
125
+ const getBlob = () => Promise.resolve(blob);
126
+ const downloadAndFinish = () => {
127
+ (0, exports.downloadBlob)(blob, job.outName);
128
+ markClientJobDone(job.id, metadata, getBlob);
129
+ };
130
+ if (window.remotion_isReadOnlyStudio) {
131
+ downloadAndFinish();
132
+ }
133
+ else {
134
+ try {
135
+ await (0, save_render_output_1.saveOutputFile)({ blob, filePath: job.outName });
136
+ await (0, save_render_output_1.registerClientRender)({
137
+ id: job.id,
138
+ type: job.type,
139
+ compositionId: job.compositionId,
140
+ outName: job.outName,
141
+ startedAt: job.startedAt,
142
+ deletedOutputLocation: false,
143
+ metadata,
144
+ });
145
+ markClientJobDone(job.id, metadata);
146
+ }
147
+ catch (err) {
148
+ // eslint-disable-next-line no-console
149
+ console.error('Failed to save render output, falling back to browser download.', err);
150
+ downloadAndFinish();
151
+ }
152
+ }
124
153
  }
125
154
  catch (err) {
126
155
  if (abortController.signal.aborted) {
@@ -135,6 +164,7 @@ const ClientRenderQueueProcessor = () => {
135
164
  processStillJob,
136
165
  processVideoJob,
137
166
  updateClientJobProgress,
167
+ markClientJobSaving,
138
168
  markClientJobDone,
139
169
  markClientJobFailed,
140
170
  markClientJobCancelled,
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+ import type { ClientRenderJob } from './client-side-render-types';
3
+ export declare const RenderQueueDownloadItem: React.FC<{
4
+ readonly job: ClientRenderJob;
5
+ }>;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RenderQueueDownloadItem = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const InlineAction_1 = require("../InlineAction");
7
+ const NotificationCenter_1 = require("../Notifications/NotificationCenter");
8
+ const ClientRenderQueueProcessor_1 = require("./ClientRenderQueueProcessor");
9
+ const RenderQueueDownloadItem = ({ job }) => {
10
+ const onClick = (0, react_1.useCallback)((e) => {
11
+ e.stopPropagation();
12
+ if (job.status !== 'done' || !job.getBlob) {
13
+ return;
14
+ }
15
+ job
16
+ .getBlob()
17
+ .then((blob) => {
18
+ (0, ClientRenderQueueProcessor_1.downloadBlob)(blob, job.outName);
19
+ })
20
+ .catch((err) => {
21
+ (0, NotificationCenter_1.showNotification)(`Could not download file: ${err.message}`, 2000);
22
+ });
23
+ }, [job]);
24
+ const icon = (0, react_1.useMemo)(() => {
25
+ return {
26
+ height: 12,
27
+ color: 'currentColor',
28
+ };
29
+ }, []);
30
+ const renderAction = (0, react_1.useCallback)((color) => {
31
+ return ((0, jsx_runtime_1.jsx)("svg", { style: icon, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 512 512", children: (0, jsx_runtime_1.jsx)("path", { fill: color, d: "M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 242.7-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7 288 32zM64 352c-35.3 0-64 28.7-64 64l0 32c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-32c0-35.3-28.7-64-64-64l-101.5 0-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352 64 352zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z" }) }));
32
+ }, [icon]);
33
+ return ((0, jsx_runtime_1.jsx)(InlineAction_1.InlineAction, { renderAction: renderAction, onClick: onClick, title: "Download" }));
34
+ };
35
+ exports.RenderQueueDownloadItem = RenderQueueDownloadItem;
@@ -7,9 +7,11 @@ const remotion_1 = require("remotion");
7
7
  const colors_1 = require("../../helpers/colors");
8
8
  const url_state_1 = require("../../helpers/url-state");
9
9
  const layout_1 = require("../layout");
10
+ const client_side_render_types_1 = require("./client-side-render-types");
10
11
  const context_1 = require("./context");
11
12
  const RenderQueueCancelledMessage_1 = require("./RenderQueueCancelledMessage");
12
13
  const RenderQueueCopyToClipboard_1 = require("./RenderQueueCopyToClipboard");
14
+ const RenderQueueDownloadItem_1 = require("./RenderQueueDownloadItem");
13
15
  const RenderQueueError_1 = require("./RenderQueueError");
14
16
  const RenderQueueItemCancelButton_1 = require("./RenderQueueItemCancelButton");
15
17
  const RenderQueueItemStatus_1 = require("./RenderQueueItemStatus");
@@ -18,6 +20,7 @@ const RenderQueueOutputName_1 = require("./RenderQueueOutputName");
18
20
  const RenderQueueProgressMessage_1 = require("./RenderQueueProgressMessage");
19
21
  const RenderQueueRemoveItem_1 = require("./RenderQueueRemoveItem");
20
22
  const RenderQueueRepeat_1 = require("./RenderQueueRepeat");
23
+ const RenderQueueSavingMessage_1 = require("./RenderQueueSavingMessage");
21
24
  const container = {
22
25
  padding: 12,
23
26
  display: 'flex',
@@ -69,24 +72,41 @@ const RenderQueueItem = ({ job, selected }) => {
69
72
  (_a = document
70
73
  .querySelector(`.${SELECTED_CLASSNAME}`)) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth' });
71
74
  }, []);
75
+ const clientBlobInfo = (0, react_1.useMemo)(() => {
76
+ if (!isClientJob || job.status !== 'done' || !job.getBlob) {
77
+ return null;
78
+ }
79
+ return {
80
+ getBlob: job.getBlob,
81
+ width: job.metadata.width,
82
+ height: job.metadata.height,
83
+ sizeInBytes: job.metadata.sizeInBytes,
84
+ };
85
+ }, [isClientJob, job]);
72
86
  const onClick = (0, react_1.useCallback)(() => {
73
87
  if (job.status !== 'done') {
74
88
  return;
75
89
  }
76
- if (isClientJob) {
77
- const clientJob = job;
78
- setCanvasContent({
79
- type: 'output-blob',
80
- displayName: job.outName,
81
- getBlob: clientJob.getBlob,
82
- width: clientJob.metadata.width,
83
- height: clientJob.metadata.height,
84
- sizeInBytes: clientJob.metadata.sizeInBytes,
85
- });
90
+ // Cannot show folders
91
+ if (!isClientJob && job.type === 'sequence') {
86
92
  return;
87
93
  }
88
- // Cannot show folders
89
- if (job.type === 'sequence') {
94
+ if (clientBlobInfo) {
95
+ setCanvasContent((c) => {
96
+ const isAlreadySelected = c && c.type === 'output-blob' && c.displayName === job.outName;
97
+ if (isAlreadySelected && !selected) {
98
+ scrollCurrentIntoView();
99
+ return c;
100
+ }
101
+ return {
102
+ type: 'output-blob',
103
+ displayName: job.outName,
104
+ getBlob: clientBlobInfo.getBlob,
105
+ width: clientBlobInfo.width,
106
+ height: clientBlobInfo.height,
107
+ sizeInBytes: clientBlobInfo.sizeInBytes,
108
+ };
109
+ });
90
110
  return;
91
111
  }
92
112
  setCanvasContent((c) => {
@@ -98,14 +118,26 @@ const RenderQueueItem = ({ job, selected }) => {
98
118
  return { type: 'output', path: `/${job.outName}` };
99
119
  });
100
120
  (0, url_state_1.pushUrl)(`/outputs/${job.outName}`);
101
- }, [job, isClientJob, scrollCurrentIntoView, selected, setCanvasContent]);
121
+ }, [
122
+ job,
123
+ isClientJob,
124
+ clientBlobInfo,
125
+ scrollCurrentIntoView,
126
+ selected,
127
+ setCanvasContent,
128
+ ]);
102
129
  (0, react_1.useEffect)(() => {
103
130
  if (selected) {
104
131
  scrollCurrentIntoView();
105
132
  }
106
133
  }, [scrollCurrentIntoView, selected]);
107
- return ((0, jsx_runtime_1.jsxs)(layout_1.Row, { onPointerEnter: onPointerEnter, onPointerLeave: onPointerLeave, style: containerStyle, align: "center", onClick: onClick, className: selected ? SELECTED_CLASSNAME : undefined, children: [(0, jsx_runtime_1.jsx)(RenderQueueItemStatus_1.RenderQueueItemStatus, { job: job }), (0, jsx_runtime_1.jsx)(layout_1.Spacing, { x: 1 }), (0, jsx_runtime_1.jsxs)("div", { style: right, children: [(0, jsx_runtime_1.jsx)("div", { style: title, children: job.compositionId }), (0, jsx_runtime_1.jsx)("div", { style: subtitle, children: job.status === 'done' ? ((0, jsx_runtime_1.jsx)(RenderQueueOutputName_1.RenderQueueOutputName, { job: job })) : job.status === 'failed' ? ((0, jsx_runtime_1.jsx)(RenderQueueError_1.RenderQueueError, { job: job })) : job.status === 'running' ? ((0, jsx_runtime_1.jsx)(RenderQueueProgressMessage_1.RenderQueueProgressMessage, { job: job })) : job.status === 'cancelled' ? ((0, jsx_runtime_1.jsx)(RenderQueueCancelledMessage_1.RenderQueueCancelledMessage, {})) : null })] }), (0, jsx_runtime_1.jsx)(layout_1.Spacing, { x: 1 }), !isClientJob && (0, RenderQueueCopyToClipboard_1.supportsCopyingToClipboard)(job) ? ((0, jsx_runtime_1.jsx)(RenderQueueCopyToClipboard_1.RenderQueueCopyToClipboard, { job: job })) : null, job.status === 'done' ||
108
- job.status === 'failed' ||
109
- job.status === 'cancelled' ? ((0, jsx_runtime_1.jsx)(RenderQueueRepeat_1.RenderQueueRepeatItem, { job: job })) : null, job.status === 'running' ? ((0, jsx_runtime_1.jsx)(RenderQueueItemCancelButton_1.RenderQueueCancelButton, { job: job })) : ((0, jsx_runtime_1.jsx)(RenderQueueRemoveItem_1.RenderQueueRemoveItem, { job: job })), job.status === 'done' && !isClientJob ? ((0, jsx_runtime_1.jsx)(RenderQueueOpenInFolder_1.RenderQueueOpenInFinderItem, { job: job })) : null] }));
134
+ const canCopyToClipboard = job.status === 'done' &&
135
+ !isClientJob &&
136
+ (0, RenderQueueCopyToClipboard_1.supportsCopyingToClipboard)(job);
137
+ const canRepeat = (job.status === 'done' ||
138
+ job.status === 'failed' ||
139
+ job.status === 'cancelled') &&
140
+ !((0, context_1.isClientRenderJob)(job) && (0, client_side_render_types_1.isRestoredClientJob)(job));
141
+ return ((0, jsx_runtime_1.jsxs)(layout_1.Row, { onPointerEnter: onPointerEnter, onPointerLeave: onPointerLeave, style: containerStyle, align: "center", onClick: onClick, className: selected ? SELECTED_CLASSNAME : undefined, children: [(0, jsx_runtime_1.jsx)(RenderQueueItemStatus_1.RenderQueueItemStatus, { job: job }), (0, jsx_runtime_1.jsx)(layout_1.Spacing, { x: 1 }), (0, jsx_runtime_1.jsxs)("div", { style: right, children: [(0, jsx_runtime_1.jsx)("div", { style: title, children: job.compositionId }), (0, jsx_runtime_1.jsx)("div", { style: subtitle, children: job.status === 'done' ? ((0, jsx_runtime_1.jsx)(RenderQueueOutputName_1.RenderQueueOutputName, { job: job })) : job.status === 'failed' ? ((0, jsx_runtime_1.jsx)(RenderQueueError_1.RenderQueueError, { job: job })) : job.status === 'running' ? ((0, jsx_runtime_1.jsx)(RenderQueueProgressMessage_1.RenderQueueProgressMessage, { job: job })) : job.status === 'saving' ? ((0, jsx_runtime_1.jsx)(RenderQueueSavingMessage_1.RenderQueueSavingMessage, {})) : job.status === 'cancelled' ? ((0, jsx_runtime_1.jsx)(RenderQueueCancelledMessage_1.RenderQueueCancelledMessage, {})) : null })] }), (0, jsx_runtime_1.jsx)(layout_1.Spacing, { x: 1 }), canCopyToClipboard ? ((0, jsx_runtime_1.jsx)(RenderQueueCopyToClipboard_1.RenderQueueCopyToClipboard, { job: job })) : null, canRepeat ? (0, jsx_runtime_1.jsx)(RenderQueueRepeat_1.RenderQueueRepeatItem, { job: job }) : null, job.status === 'running' ? ((0, jsx_runtime_1.jsx)(RenderQueueItemCancelButton_1.RenderQueueCancelButton, { job: job })) : ((0, jsx_runtime_1.jsx)(RenderQueueRemoveItem_1.RenderQueueRemoveItem, { job: job })), job.status === 'done' ? (clientBlobInfo ? ((0, jsx_runtime_1.jsx)(RenderQueueDownloadItem_1.RenderQueueDownloadItem, { job: job })) : ((0, jsx_runtime_1.jsx)(RenderQueueOpenInFolder_1.RenderQueueOpenInFinderItem, { job: job }))) : null] }));
110
142
  };
111
143
  exports.RenderQueueItem = RenderQueueItem;
@@ -88,6 +88,9 @@ const RenderQueueItemStatus = ({ job }) => {
88
88
  }
89
89
  return ((0, jsx_runtime_1.jsx)("button", { type: "button", style: invisibleStyle, onClick: onClick, children: (0, jsx_runtime_1.jsx)(CircularProgress_1.CircularProgress, { progress: Math.max(0.07, progressValue) }) }));
90
90
  }
91
+ if (job.status === 'saving') {
92
+ return ((0, jsx_runtime_1.jsx)("button", { type: "button", style: invisibleStyle, onClick: onClick, children: (0, jsx_runtime_1.jsx)(CircularProgress_1.CircularProgress, { progress: 0.95 }) }));
93
+ }
91
94
  if (job.status === 'cancelled') {
92
95
  return ((0, jsx_runtime_1.jsx)("svg", { style: iconStyle, viewBox: "0 0 512 512", children: (0, jsx_runtime_1.jsx)("path", { fill: colors_1.FAIL_COLOR, d: "M0 160V352L160 512H352L512 352V160L352 0H160L0 160zm353.9 32l-17 17-47 47 47 47 17 17L320 353.9l-17-17-47-47-47 47-17 17L158.1 320l17-17 47-47-47-47-17-17L192 158.1l17 17 47 47 47-47 17-17L353.9 192z" }) }));
93
96
  }
@@ -1,5 +1,5 @@
1
- import type { RenderJob } from '@remotion/studio-shared';
2
1
  import React from 'react';
2
+ import type { AnyRenderJob } from './context';
3
3
  export declare const RenderQueueOpenInFinderItem: React.FC<{
4
- readonly job: RenderJob;
4
+ readonly job: AnyRenderJob;
5
5
  }>;
@@ -3,13 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RenderQueueOutputName = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
- const context_1 = require("./context");
7
6
  const item_style_1 = require("./item-style");
8
7
  const RenderQueueOutputName = ({ job }) => {
9
- const isClientJob = (0, context_1.isClientRenderJob)(job);
10
- const deletedOutputLocation = isClientJob
11
- ? false
12
- : job.deletedOutputLocation;
8
+ const deletedOutputLocation = 'deletedOutputLocation' in job && job.deletedOutputLocation;
13
9
  const style = (0, react_1.useMemo)(() => {
14
10
  return {
15
11
  ...item_style_1.renderQueueItemSubtitleStyle,
@@ -19,9 +15,6 @@ const RenderQueueOutputName = ({ job }) => {
19
15
  };
20
16
  }, [deletedOutputLocation]);
21
17
  const getTitle = () => {
22
- if (isClientJob) {
23
- return `Downloaded as ${job.outName}`;
24
- }
25
18
  if (deletedOutputLocation) {
26
19
  return 'File was deleted';
27
20
  }
@@ -4,6 +4,7 @@ exports.RenderQueueRemoveItem = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const remotion_1 = require("remotion");
7
+ const save_render_output_1 = require("../../api/save-render-output");
7
8
  const InlineAction_1 = require("../InlineAction");
8
9
  const NotificationCenter_1 = require("../Notifications/NotificationCenter");
9
10
  const actions_1 = require("./actions");
@@ -17,12 +18,13 @@ const RenderQueueRemoveItem = ({ job }) => {
17
18
  e.stopPropagation();
18
19
  if (isClientJob) {
19
20
  if (canvasContent &&
20
- canvasContent.type === 'output-blob' &&
21
+ canvasContent.type === 'output' &&
21
22
  job.status === 'done' &&
22
- canvasContent.getBlob === job.getBlob) {
23
+ canvasContent.path === `/${job.outName}`) {
23
24
  setCanvasContent(null);
24
25
  }
25
26
  removeClientJob(job.id);
27
+ (0, save_render_output_1.unregisterClientRender)(job.id).catch(() => { });
26
28
  (0, NotificationCenter_1.showNotification)('Removed job', 2000);
27
29
  return;
28
30
  }
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare const RenderQueueSavingMessage: React.FC;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RenderQueueSavingMessage = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const item_style_1 = require("./item-style");
6
+ const savingStyle = {
7
+ ...item_style_1.renderQueueItemSubtitleStyle,
8
+ cursor: 'default',
9
+ };
10
+ const RenderQueueSavingMessage = () => {
11
+ return (0, jsx_runtime_1.jsx)("span", { style: savingStyle, children: "Saving to out/..." });
12
+ };
13
+ exports.RenderQueueSavingMessage = RenderQueueSavingMessage;
@@ -1,3 +1,4 @@
1
+ import type { CompletedClientRender } from '@remotion/studio-shared';
1
2
  import type { RenderStillOnWebImageFormat, WebRendererAudioCodec, WebRendererContainer, WebRendererQuality, WebRendererVideoCodec } from '@remotion/web-renderer';
2
3
  import type { LogLevel } from 'remotion';
3
4
  export type ClientRenderJobProgress = {
@@ -6,20 +7,17 @@ export type ClientRenderJobProgress = {
6
7
  totalFrames: number;
7
8
  };
8
9
  export type GetBlobCallback = () => Promise<Blob>;
9
- export type ClientRenderMetadata = {
10
- width: number;
11
- height: number;
12
- sizeInBytes: number;
13
- };
14
10
  type ClientRenderJobDynamicStatus = {
15
11
  status: 'idle';
16
12
  } | {
17
13
  status: 'running';
18
14
  progress: ClientRenderJobProgress;
15
+ } | {
16
+ status: 'saving';
19
17
  } | {
20
18
  status: 'done';
21
- getBlob: GetBlobCallback;
22
- metadata: ClientRenderMetadata;
19
+ getBlob?: GetBlobCallback;
20
+ metadata: CompletedClientRender['metadata'];
23
21
  } | {
24
22
  status: 'cancelled';
25
23
  } | {
@@ -60,5 +58,10 @@ export type ClientVideoRenderJob = ClientRenderJobBase & {
60
58
  transparent: boolean;
61
59
  muted: boolean;
62
60
  } & ClientRenderJobDynamicStatus;
63
- export type ClientRenderJob = ClientStillRenderJob | ClientVideoRenderJob;
61
+ export type RestoredClientRenderJob = CompletedClientRender & {
62
+ status: 'done';
63
+ getBlob?: GetBlobCallback;
64
+ };
65
+ export type ClientRenderJob = ClientStillRenderJob | ClientVideoRenderJob | RestoredClientRenderJob;
66
+ export declare const isRestoredClientJob: (job: ClientRenderJob) => job is RestoredClientRenderJob;
64
67
  export {};
@@ -1,2 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isRestoredClientJob = void 0;
4
+ const isRestoredClientJob = (job) => {
5
+ return !('inputProps' in job);
6
+ };
7
+ exports.isRestoredClientJob = isRestoredClientJob;
@@ -1,10 +1,11 @@
1
- import type { RenderJob } from '@remotion/studio-shared';
1
+ import type { CompletedClientRender, RenderJob } from '@remotion/studio-shared';
2
2
  import React from 'react';
3
3
  import { type AddClientStillJobParams, type AddClientVideoJobParams, type CompositionRef } from './client-render-queue';
4
- import type { ClientRenderJob, ClientRenderJobProgress, ClientRenderMetadata, GetBlobCallback } from './client-side-render-types';
4
+ import type { ClientRenderJob, ClientRenderJobProgress, GetBlobCallback } from './client-side-render-types';
5
5
  declare global {
6
6
  interface Window {
7
7
  remotion_initialRenderQueue: RenderJob[] | null;
8
+ remotion_initialClientRenders: CompletedClientRender[] | null;
8
9
  }
9
10
  }
10
11
  export type AnyRenderJob = RenderJob | ClientRenderJob;
@@ -16,7 +17,8 @@ type RenderQueueContextType = {
16
17
  addClientStillJob: (params: AddClientStillJobParams, compositionRef: CompositionRef) => string;
17
18
  addClientVideoJob: (params: AddClientVideoJobParams, compositionRef: CompositionRef) => string;
18
19
  updateClientJobProgress: (jobId: string, progress: ClientRenderJobProgress) => void;
19
- markClientJobDone: (jobId: string, getBlob: GetBlobCallback, metadata: ClientRenderMetadata) => void;
20
+ markClientJobSaving: (jobId: string) => void;
21
+ markClientJobDone: (jobId: string, metadata: CompletedClientRender['metadata'], getBlob?: GetBlobCallback) => void;
20
22
  markClientJobFailed: (jobId: string, error: Error) => void;
21
23
  markClientJobCancelled: (jobId: string) => void;
22
24
  removeClientJob: (jobId: string) => void;
@@ -28,6 +30,7 @@ type RenderQueueContextType = {
28
30
  export declare const RenderQueueContext: React.Context<RenderQueueContextType>;
29
31
  export declare const renderJobsRef: React.RefObject<{
30
32
  updateRenderJobs: (jobs: RenderJob[]) => void;
33
+ updateClientRenders: (renders: CompletedClientRender[]) => void;
31
34
  } | null>;
32
35
  export declare const RenderQueueContextProvider: React.FC<{
33
36
  readonly children: React.ReactNode;
@@ -37,6 +37,11 @@ exports.RenderQueueContextProvider = exports.renderJobsRef = exports.RenderQueue
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const react_1 = __importStar(require("react"));
39
39
  const client_render_queue_1 = require("./client-render-queue");
40
+ const restorePersistedClientRenders = () => {
41
+ var _a;
42
+ const persisted = (_a = window.remotion_initialClientRenders) !== null && _a !== void 0 ? _a : [];
43
+ return persisted.map((r) => ({ ...r, status: 'done' }));
44
+ };
40
45
  const isClientRenderJob = (job) => {
41
46
  return job.type === 'client-still' || job.type === 'client-video';
42
47
  };
@@ -50,6 +55,7 @@ exports.RenderQueueContext = react_1.default.createContext({
50
55
  addClientStillJob: noopString,
51
56
  addClientVideoJob: noopString,
52
57
  updateClientJobProgress: noop,
58
+ markClientJobSaving: noop,
53
59
  markClientJobDone: noop,
54
60
  markClientJobFailed: noop,
55
61
  markClientJobCancelled: noop,
@@ -63,7 +69,7 @@ exports.renderJobsRef = (0, react_1.createRef)();
63
69
  const RenderQueueContextProvider = ({ children }) => {
64
70
  var _a;
65
71
  const [serverJobs, setServerJobs] = (0, react_1.useState)((_a = window.remotion_initialRenderQueue) !== null && _a !== void 0 ? _a : []);
66
- const [clientJobs, setClientJobs] = (0, react_1.useState)([]);
72
+ const [clientJobs, setClientJobs] = (0, react_1.useState)(restorePersistedClientRenders);
67
73
  const [currentlyProcessing, setCurrentlyProcessing] = (0, react_1.useState)(null);
68
74
  const processJobCallbackRef = (0, react_1.useRef)(null);
69
75
  // Process next job when state changes
@@ -114,12 +120,15 @@ const RenderQueueContextProvider = ({ children }) => {
114
120
  ? { ...job, status: 'running', progress }
115
121
  : job));
116
122
  }, []);
117
- const markClientJobDone = (0, react_1.useCallback)((jobId, getBlob, metadata) => {
118
- (0, client_render_queue_1.deleteAbortController)(jobId);
119
- (0, client_render_queue_1.cleanupCompositionForJob)(jobId);
123
+ const markClientJobSaving = (0, react_1.useCallback)((jobId) => {
120
124
  setClientJobs((prev) => prev.map((job) => job.id === jobId
121
- ? { ...job, status: 'done', getBlob, metadata }
125
+ ? { ...job, status: 'saving' }
122
126
  : job));
127
+ }, []);
128
+ const markClientJobDone = (0, react_1.useCallback)((jobId, metadata, getBlob) => {
129
+ (0, client_render_queue_1.deleteAbortController)(jobId);
130
+ (0, client_render_queue_1.cleanupCompositionForJob)(jobId);
131
+ setClientJobs((prev) => prev.map((job) => job.id === jobId ? { ...job, status: 'done', metadata, getBlob } : job));
123
132
  setCurrentlyProcessing(null);
124
133
  }, []);
125
134
  const markClientJobFailed = (0, react_1.useCallback)((jobId, error) => {
@@ -166,15 +175,36 @@ const RenderQueueContextProvider = ({ children }) => {
166
175
  updateRenderJobs: (newJobs) => {
167
176
  setServerJobs(newJobs);
168
177
  },
178
+ updateClientRenders: (renders) => {
179
+ setClientJobs((prev) => {
180
+ const existingIds = new Set(prev.map((j) => j.id));
181
+ const updatedPrev = prev.map((job) => {
182
+ const updated = renders.find((r) => r.id === job.id);
183
+ if (updated && job.status === 'done') {
184
+ return {
185
+ ...job,
186
+ deletedOutputLocation: updated.deletedOutputLocation,
187
+ metadata: updated.metadata,
188
+ };
189
+ }
190
+ return job;
191
+ });
192
+ const newJobs = renders
193
+ .filter((r) => !existingIds.has(r.id))
194
+ .map((r) => ({ ...r, status: 'done' }));
195
+ return [...updatedPrev, ...newJobs];
196
+ });
197
+ },
169
198
  }), []);
170
199
  const value = (0, react_1.useMemo)(() => {
171
200
  return {
172
- jobs: [...serverJobs, ...clientJobs],
201
+ jobs: [...serverJobs, ...clientJobs].sort((a, b) => a.startedAt - b.startedAt),
173
202
  serverJobs,
174
203
  clientJobs,
175
204
  addClientStillJob,
176
205
  addClientVideoJob,
177
206
  updateClientJobProgress,
207
+ markClientJobSaving,
178
208
  markClientJobDone,
179
209
  markClientJobFailed,
180
210
  markClientJobCancelled,
@@ -190,6 +220,7 @@ const RenderQueueContextProvider = ({ children }) => {
190
220
  addClientStillJob,
191
221
  addClientVideoJob,
192
222
  updateClientJobProgress,
223
+ markClientJobSaving,
193
224
  markClientJobDone,
194
225
  markClientJobFailed,
195
226
  markClientJobCancelled,