perf(canvas): 画布媒体 - 优化媒体加载、播放器与节点交互 (#357)
Merge origin/main at fb089b2 and retain canvas media performance, player layout, and interaction updates.
Co-authored-by: ddCat <2695092045@qq.com>
This commit is contained in:
@@ -2,10 +2,12 @@ import localforage from "localforage";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { getActiveUserScope } from "@/lib/user-scope";
|
||||
import { captureVideoPoster, detectVideoAudioTrackFromBlob } from "@/lib/video-poster";
|
||||
import { resourceFileUrl, resourceIdFromStorageKey, resourceStorageKey, uploadResourceFile } from "@/services/api/resources";
|
||||
import { uploadImage, type UploadedImage } from "@/services/image-storage";
|
||||
import { getCachedResourceBlob, getCachedResourceObjectUrl, primeResourceBlobCache } from "@/services/resource-blob-cache";
|
||||
|
||||
export type UploadedFile = { url: string; storageKey: string; bytes: number; mimeType: string; width?: number; height?: number; durationMs?: number };
|
||||
export type UploadedFile = { url: string; storageKey: string; bytes: number; mimeType: string; width?: number; height?: number; durationMs?: number; hasAudio?: boolean; preview?: UploadedImage };
|
||||
|
||||
const store = localforage.createInstance({ name: "infinite-canvas", storeName: "media_files" });
|
||||
const objectUrls = new Map<string, string>();
|
||||
@@ -13,13 +15,24 @@ const objectUrls = new Map<string, string>();
|
||||
export async function uploadMediaFile(input: string | Blob, prefix = "file"): Promise<UploadedFile> {
|
||||
const blob = typeof input === "string" ? await (await fetch(input)).blob() : input;
|
||||
const previewUrl = URL.createObjectURL(blob);
|
||||
const meta: { width?: number; height?: number; durationMs?: number } = blob.type.startsWith("video/") ? await readVideoMeta(previewUrl) : blob.type.startsWith("audio/") ? await readAudioMeta(previewUrl) : {};
|
||||
const captured = blob.type.startsWith("video/") ? await captureVideoPoster(previewUrl).catch(() => undefined) : undefined;
|
||||
// Browser track probes can report a false negative for MP4/MOV files.
|
||||
// Re-check only when capture did not positively confirm an audio track so
|
||||
// normal uploads avoid an extra full-blob parse.
|
||||
const parsedHasAudio = blob.type.startsWith("video/") && captured?.hasAudio !== true ? await detectVideoAudioTrackFromBlob(blob) : undefined;
|
||||
const resolvedHasAudio = parsedHasAudio ?? (captured?.hasAudio === false ? undefined : captured?.hasAudio);
|
||||
const meta: { width?: number; height?: number; durationMs?: number; hasAudio?: boolean } = captured
|
||||
? { width: captured.width, height: captured.height, durationMs: captured.durationMs, hasAudio: resolvedHasAudio }
|
||||
: blob.type.startsWith("audio/")
|
||||
? await readAudioMeta(previewUrl)
|
||||
: { hasAudio: resolvedHasAudio };
|
||||
const poster = captured?.poster ? await uploadImage(captured.poster).catch(() => undefined) : undefined;
|
||||
try {
|
||||
const kind = blob.type.startsWith("video/") ? "video" : blob.type.startsWith("audio/") ? "audio" : "file";
|
||||
const resource = await uploadResourceFile(blob, kind, { ...meta, fileName: input instanceof File ? input.name : undefined });
|
||||
await primeResourceBlobCache(resourceStorageKey(resource.id), blob).catch(() => "");
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
return { url: resource.publicUrl || resourceFileUrl(resource.id), storageKey: resourceStorageKey(resource.id), bytes: resource.size || blob.size, mimeType: resource.mimeType || blob.type || "application/octet-stream", width: resource.width || meta.width, height: resource.height || meta.height, durationMs: resource.durationMs || meta.durationMs };
|
||||
return { url: resource.publicUrl || resourceFileUrl(resource.id), storageKey: resourceStorageKey(resource.id), bytes: resource.size || blob.size, mimeType: resource.mimeType || blob.type || "application/octet-stream", width: resource.width || meta.width, height: resource.height || meta.height, durationMs: resource.durationMs || meta.durationMs, hasAudio: meta.hasAudio, preview: poster };
|
||||
} catch {
|
||||
// OSS is optional during local/self-hosted setup. Keep the existing local fallback.
|
||||
}
|
||||
@@ -27,7 +40,7 @@ export async function uploadMediaFile(input: string | Blob, prefix = "file"): Pr
|
||||
await store.setItem(storageKey, blob);
|
||||
const url = previewUrl;
|
||||
objectUrls.set(storageKey, url);
|
||||
return { url, storageKey, bytes: blob.size, mimeType: blob.type || "application/octet-stream", ...meta };
|
||||
return { url, storageKey, bytes: blob.size, mimeType: blob.type || "application/octet-stream", ...meta, preview: poster };
|
||||
}
|
||||
|
||||
export async function resolveMediaUrl(storageKey?: string, fallback = "") {
|
||||
@@ -89,16 +102,6 @@ export function collectMediaStorageKeys(value: unknown, keys = new Set<string>()
|
||||
return keys;
|
||||
}
|
||||
|
||||
function readVideoMeta(url: string) {
|
||||
return new Promise<{ width: number; height: number; durationMs?: number }>((resolve) => {
|
||||
const video = document.createElement("video");
|
||||
const done = () => resolve({ width: video.videoWidth || 1280, height: video.videoHeight || 720, durationMs: Number.isFinite(video.duration) ? Math.round(video.duration * 1000) : undefined });
|
||||
video.onloadedmetadata = done;
|
||||
video.onerror = done;
|
||||
video.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
function readAudioMeta(url: string) {
|
||||
return new Promise<{ durationMs?: number }>((resolve) => {
|
||||
const audio = document.createElement("audio");
|
||||
|
||||
Reference in New Issue
Block a user