Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 10 additions & 31 deletions apps/web/src/components/Panels/Canvas/CanvasToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ import {
} from './canvasInputPolicy.ts';
import { NODE_ICON } from '../../../config/nodeIcons.ts';
import useCanvasStore from '../../../store/canvasStore.ts';
import { detectNodeType, detectOfficeFormat } from '../../../utils/io/media.ts';
import {
detectNodeType,
detectOfficeFormat,
getImageDimensionsFromBlob,
getVideoDimensionsFromBlob,
} from '../../../utils/io/media.ts';
import { Button } from '../../Common/Button.tsx';
import { Modal } from '../../Common/Modal.tsx';
import { Popover } from '../../Common/Popover.tsx';
Expand Down Expand Up @@ -225,34 +230,6 @@ export const NodeToolbar = ({ activeTool, onToolChange }: NodeToolbarProps) => {
return { x: rect.left, y: rect.top };
};

const getImageDimensions = (
file: File,
): Promise<{ width: number; height: number }> => {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = URL.createObjectURL(file);
img.onload = () => {
resolve({ width: img.naturalWidth, height: img.naturalHeight });
URL.revokeObjectURL(img.src);
};
img.onerror = reject;
});
};

const getVideoDimensions = (
file: File,
): Promise<{ width: number; height: number }> => {
return new Promise((resolve, reject) => {
const video = document.createElement('video');
video.src = URL.createObjectURL(file);
video.onloadedmetadata = () => {
resolve({ width: video.videoWidth, height: video.videoHeight });
URL.revokeObjectURL(video.src);
};
video.onerror = reject;
});
};

const handleFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
Expand All @@ -273,14 +250,16 @@ export const NodeToolbar = ({ activeTool, onToolChange }: NodeToolbarProps) => {
if (type === 'image') {
const [uploadedUrl, dims] = await Promise.all([
uploadImage(file, canvasId),
getImageDimensions(file),
getImageDimensionsFromBlob(file).catch(() => undefined),
]);
url = uploadedUrl;
naturalDimensions = dims;
} else if (type === 'video') {
const [uploadedUrl, dims] = await Promise.all([
uploadVideo(file, canvasId),
getVideoDimensions(file),
// A codec the browser cannot decode still deserves a node — it
// just falls back to the default size.
getVideoDimensionsFromBlob(file).catch(() => undefined),
]);
url = uploadedUrl;
naturalDimensions = dims;
Expand Down
9 changes: 8 additions & 1 deletion apps/web/src/handler/canvasCommand/nodeInputBuilders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
detectNodeTypeFromMime,
detectOfficeFormat,
getImageDimensionsFromBlob,
getVideoDimensionsFromBlob,
normalizeUrl,
} from '../../utils/io/media';

Expand Down Expand Up @@ -57,11 +58,17 @@ export async function uploadFileToNodeInput(
}

if (type === 'video') {
const src = await uploadVideo(file, canvasId);
const [src, naturalDimensions] = await Promise.all([
uploadVideo(file, canvasId),
// A codec the browser cannot decode still deserves a node — it just
// falls back to the default size instead of the real aspect ratio.
getVideoDimensionsFromBlob(file).catch(() => undefined),
]);
return {
nodeType: 'video',
placementPoint,
data: { src, label: file.name, origin },
naturalDimensions,
};
}

Expand Down
25 changes: 25 additions & 0 deletions apps/web/src/utils/io/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,28 @@ export const getImageDimensionsFromBlob = (
};
img.src = url;
});

/**
* Get intrinsic dimensions of a video from a Blob/File.
*
* Only the container metadata is needed, hence `preload = 'metadata'`.
* Callers use this to size a new video node to the real aspect ratio —
* without it the node falls back to the generic 4:3 default.
*/
export const getVideoDimensionsFromBlob = (
blob: Blob,
): Promise<{ width: number; height: number }> =>
new Promise((resolve, reject) => {
const url = URL.createObjectURL(blob);
const video = document.createElement('video');
video.preload = 'metadata';
video.onloadedmetadata = () => {
resolve({ width: video.videoWidth, height: video.videoHeight });
URL.revokeObjectURL(url);
};
video.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load video'));
};
video.src = url;
});
Loading