fix(media): 视频播放 - 资源视频改走 playback 变体,失败后刷新签名并重试
画布预览、生成回填和项目水合都解析浏览器可播地址;播放失败时绕过展示缓存,等待转码完成后强制刷新 playback 地址。
This commit is contained in:
@@ -120,6 +120,11 @@ export type ResourceAccess = {
|
||||
originalHeight?: number;
|
||||
};
|
||||
|
||||
export type ResourceAccessFetchOptions = {
|
||||
/** Bypass both in-memory and persisted display-address caches. */
|
||||
forceRefresh?: boolean;
|
||||
};
|
||||
|
||||
type ResourceAccessBatchItem = { resourceId: string; access?: ResourceAccess; error?: { msg?: string } };
|
||||
type CachedResourceAccess = { value: ResourceAccess; expiresAt: number };
|
||||
const accessCache = new Map<string, CachedResourceAccess>();
|
||||
@@ -373,12 +378,24 @@ function assertResourceRequestCurrent(generation: number, scope: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getResourceAccess(storageKey: string | undefined, purpose: ResourceAccessPurpose = "display", variant: ResourceAccessVariant = "original", downloadName = "") {
|
||||
export async function getResourceAccess(storageKey: string | undefined, purpose: ResourceAccessPurpose = "display", variant: ResourceAccessVariant = "original", downloadName = "", fetchOptions: ResourceAccessFetchOptions = {}) {
|
||||
const id = resourceIdFromStorageKey(storageKey);
|
||||
if (!id) throw new Error("当前媒体尚未上传到后端资源存储");
|
||||
const scope = getActiveUserScope();
|
||||
const generation = accessGeneration;
|
||||
const key = `${scope}:${id}:${purpose}:${variant}:${downloadName}`;
|
||||
const forceRefresh = fetchOptions.forceRefresh === true;
|
||||
if (forceRefresh) {
|
||||
accessCache.delete(key);
|
||||
accessRequests.delete(key);
|
||||
if (purpose === "display") {
|
||||
try {
|
||||
await localForageStorageForScope(scope).removeItem(persistedDisplayAccessKey(id, variant));
|
||||
} catch {
|
||||
// Persisted address caching is optional; the network request below remains authoritative.
|
||||
}
|
||||
}
|
||||
}
|
||||
const cached = accessCache.get(key);
|
||||
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
||||
const pending = accessRequests.get(key);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { captureVideoPoster } from "@/lib/video-poster";
|
||||
import { resolveMediaUrl } from "@/services/file-storage";
|
||||
import { resolveVideoMediaUrl } from "@/services/file-storage";
|
||||
import { uploadImage } from "@/services/image-storage";
|
||||
import type { CanvasNodeData, CanvasNodeMetadata } from "@/types/canvas";
|
||||
|
||||
@@ -24,7 +24,7 @@ export function hydrateCanvasVideoPreview(node: CanvasNodeData, signal?: AbortSi
|
||||
async function generateCanvasVideoPreview(node: CanvasNodeData, signal?: AbortSignal): Promise<HydratedCanvasVideoPreview | null> {
|
||||
await waitForBrowserIdle(signal);
|
||||
throwIfAborted(signal);
|
||||
const source = await resolveMediaUrl(node.metadata?.storageKey, node.metadata?.content || "");
|
||||
const source = await resolveVideoMediaUrl(node.metadata?.storageKey, node.metadata?.content || "");
|
||||
if (!source) return null;
|
||||
const captured = await captureVideoPoster(source, { signal, maxWidth: 400 });
|
||||
throwIfAborted(signal);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { nanoid } from "nanoid";
|
||||
|
||||
import { getActiveUserScope } from "@/lib/user-scope";
|
||||
import { captureVideoPoster, detectVideoAudioTrackFromBlob } from "@/lib/video-poster";
|
||||
import { getResourceAccess, resolveResourceAccessURL, resourceFileUrl, resourceIdFromStorageKey, resourceStorageKey, ResourceUploadError, uploadResourceFile } from "@/services/api/resources";
|
||||
import { getResourceAccess, resolveResourceAccessURL, resourceFileUrl, resourceIdFromStorageKey, resourceStorageKey, ResourceUploadError, uploadResourceFile, type ResourceAccessVariant } from "@/services/api/resources";
|
||||
import { uploadImage, type UploadedImage } from "@/services/image-storage";
|
||||
import { getCachedResourceBlob, primeResourceBlobCache } from "@/services/resource-blob-cache";
|
||||
|
||||
@@ -93,7 +93,17 @@ export async function uploadMediaFile(input: Blob, prefix = "file", onProgress?:
|
||||
// 缓存只影响后续读取性能,服务端资源已经成功落盘,不得把缓存失败误报为上传失败。
|
||||
console.warn("预热媒体缓存失败,服务端资源已保存", { resourceId: resource.id, error });
|
||||
}
|
||||
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 };
|
||||
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 (error) {
|
||||
// 与图片上传同一套判定:永久性失败必须当场暴露,不能混进“稍后自动同步”。
|
||||
if (error instanceof ResourceUploadError && error.permanent) throw error;
|
||||
@@ -111,12 +121,12 @@ export async function uploadMediaFile(input: Blob, prefix = "file", onProgress?:
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveMediaUrl(storageKey?: string, fallback = "") {
|
||||
export async function resolveMediaUrl(storageKey?: string, fallback = "", variant: ResourceAccessVariant = "original") {
|
||||
if (!storageKey) return fallback;
|
||||
const resourceId = resourceIdFromStorageKey(storageKey);
|
||||
if (resourceId) {
|
||||
// 展示直接命中 OSS/CDN;平台资源文件接口只保留给私有源站代理或本地存储兜底。
|
||||
return resolveResourceAccessURL((await getResourceAccess(storageKey, "display")).url);
|
||||
return resolveResourceAccessURL((await getResourceAccess(storageKey, "display", variant)).url);
|
||||
}
|
||||
const cached = objectUrls.get(storageKey);
|
||||
if (cached) return cached;
|
||||
@@ -127,6 +137,11 @@ export async function resolveMediaUrl(storageKey?: string, fallback = "") {
|
||||
return url;
|
||||
}
|
||||
|
||||
/** Resolve a video through the browser-compatible playback variant when one exists. */
|
||||
export function resolveVideoMediaUrl(storageKey?: string, fallback = "") {
|
||||
return resolveMediaUrl(storageKey, fallback, "playback");
|
||||
}
|
||||
|
||||
export async function getMediaBlob(storageKey: string) {
|
||||
if (resourceIdFromStorageKey(storageKey)) return getCachedResourceBlob(storageKey);
|
||||
return store.getItem<Blob>(storageKey);
|
||||
|
||||
Reference in New Issue
Block a user