mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Canvas/Video: improve Blobs support
This commit is contained in:
@@ -127,7 +127,7 @@ export function CameraCaptureModal(props: {
|
||||
|
||||
const handleVideoDownloadClicked = React.useCallback(async () => {
|
||||
if (!videoRef.current) return;
|
||||
await downloadVideoFrame(videoRef.current, 'camera', 'image/jpeg', 0.98);
|
||||
await downloadVideoFrame(videoRef.current, 'camera', 'image/jpeg', 0.98).catch(alert);
|
||||
}, [videoRef]);
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +47,34 @@ export async function asyncCanvasToBlob(
|
||||
return new Promise((resolve) => canvas.toBlob(resolve, imageFormat, imageQuality));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Blob object representing the image contained in the canvas, with format validation and fallback
|
||||
* @param canvas The canvas element to convert
|
||||
* @param requestedMimeType Desired MIME type - browsers are required to support image/png; many will support additional formats including image/jpeg and some may support image/webp.
|
||||
* @param imageQuality Quality for lossy formats (0-1) (image/jpeg or image/webp)
|
||||
* @param debugLabel Label for debugging
|
||||
*/
|
||||
export async function asyncCanvasToBlobWithValidation(
|
||||
canvas: HTMLCanvasElement,
|
||||
requestedMimeType: string,
|
||||
imageQuality: undefined | number,
|
||||
debugLabel?: string,
|
||||
): Promise<{ blob: Blob; actualMimeType: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob)
|
||||
return reject(new Error(`Failed to convert canvas to blob with format '${requestedMimeType}'`));
|
||||
|
||||
// Warn if the actual MIME type differs from the requested one
|
||||
if (debugLabel && blob.type !== requestedMimeType)
|
||||
console.warn(`[DEV] ${debugLabel}: requested MIME type "${requestedMimeType}" was not used. Actual MIME type is "${blob.type}".`);
|
||||
|
||||
resolve({ blob, actualMimeType: blob.type });
|
||||
}, requestedMimeType, imageQuality);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function renderVideoFrameToNewCanvas(videoElement: HTMLVideoElement): HTMLCanvasElement {
|
||||
// paint the video on a canvas, to save it
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Also see imageUtils.ts for more image-related functions.
|
||||
*/
|
||||
|
||||
import { asyncCanvasToBlob, renderVideoFrameToNewCanvas } from './canvasUtils';
|
||||
import { asyncCanvasToBlobWithValidation, renderVideoFrameToNewCanvas } from './canvasUtils';
|
||||
import { downloadBlob } from './downloadUtils';
|
||||
import { prettyTimestampForFilenames } from './timeUtils';
|
||||
|
||||
@@ -20,10 +20,13 @@ type AllowedFormats = 'image/png' | 'image/jpeg';
|
||||
export async function downloadVideoFrame(videoElement: HTMLVideoElement, prefixName: string, imageFormat: AllowedFormats, imageQuality?: number) {
|
||||
// Video -> Canvas -> Blob
|
||||
const renderedFrame: HTMLCanvasElement = renderVideoFrameToNewCanvas(videoElement);
|
||||
const blob: Blob | null = await asyncCanvasToBlob(renderedFrame, imageFormat, imageQuality);
|
||||
if (!blob) throw new Error('Failed to render video frame to Blob.');
|
||||
// Blob -> download
|
||||
downloadBlob(blob, _videoPrettyFileName(prefixName, renderedFrame, imageFormat));
|
||||
try {
|
||||
const { blob } = await asyncCanvasToBlobWithValidation(renderedFrame, imageFormat, imageQuality, 'downloadVideoFrame');
|
||||
// Blob -> download
|
||||
downloadBlob(blob, _videoPrettyFileName(prefixName, renderedFrame, imageFormat));
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to download video frame: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,14 +35,24 @@ export async function downloadVideoFrame(videoElement: HTMLVideoElement, prefixN
|
||||
export async function renderVideoFrameAsFile(videoElement: HTMLVideoElement, prefixName: string, imageFormat: AllowedFormats, imageQuality?: number): Promise<File> {
|
||||
// Video -> Canvas -> Blob
|
||||
const renderedFrame: HTMLCanvasElement = renderVideoFrameToNewCanvas(videoElement);
|
||||
const blob: Blob | null = await asyncCanvasToBlob(renderedFrame, imageFormat, imageQuality);
|
||||
if (!blob) throw new Error('Failed to render video frame to Blob.');
|
||||
// Blob -> File
|
||||
return new File([blob], _videoPrettyFileName(prefixName, renderedFrame, imageFormat), { type: blob.type });
|
||||
try {
|
||||
const { blob, actualMimeType } = await asyncCanvasToBlobWithValidation(renderedFrame, imageFormat, imageQuality, 'renderVideoFrameAsFile');
|
||||
// Blob -> File
|
||||
return new File([blob], _videoPrettyFileName(prefixName, renderedFrame, actualMimeType), { type: actualMimeType });
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to render video frame: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function _videoPrettyFileName(prefixName: string, renderedFrame: HTMLCanvasElement, imageFormat: AllowedFormats): string {
|
||||
function _videoPrettyFileName(prefixName: string, renderedFrame: HTMLCanvasElement, imageFormat: AllowedFormats | string /* allowing for the actual mime type to be different */): string {
|
||||
const prettyResolution = `${renderedFrame.width}x${renderedFrame.height}`;
|
||||
return `${prefixName}_${prettyTimestampForFilenames()}_${prettyResolution}.${imageFormat === 'image/png' ? 'png' : 'jpg'}`;
|
||||
const extensions: { [mime: string]: string } = {
|
||||
'image/png': 'png',
|
||||
'image/jpeg': 'jpg',
|
||||
'image/webp': 'webp',
|
||||
'image/gif': 'gif',
|
||||
};
|
||||
const extension = extensions[imageFormat] || 'jpg'; // Fallback to jpg if format is not recognized
|
||||
return `${prefixName}_${prettyTimestampForFilenames()}_${prettyResolution}.${extension}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user