修复mp4解析问题
This commit is contained in:
parent
7740d611b6
commit
e423adc738
@ -19,6 +19,8 @@ const transparentLayoutKinds = new Set([
|
|||||||
]);
|
]);
|
||||||
const videoFrameStepSeconds = 1 / 24;
|
const videoFrameStepSeconds = 1 / 24;
|
||||||
const maxSeekFrameCount = 360;
|
const maxSeekFrameCount = 360;
|
||||||
|
const videoLoadTimeoutMs = 8000;
|
||||||
|
const videoSeekTimeoutMs = 5000;
|
||||||
|
|
||||||
export function isMp4File(file) {
|
export function isMp4File(file) {
|
||||||
if (!file?.name) {
|
if (!file?.name) {
|
||||||
@ -201,15 +203,15 @@ export async function mp4FileFromSource(source) {
|
|||||||
|
|
||||||
export async function readMp4VideoInfo(file) {
|
export async function readMp4VideoInfo(file) {
|
||||||
const video = await loadVideo(file);
|
const video = await loadVideo(file);
|
||||||
const info = {
|
try {
|
||||||
duration: Number.isFinite(video.duration) ? video.duration : 0,
|
return {
|
||||||
height: video.videoHeight || 0,
|
duration: Number.isFinite(video.duration) ? video.duration : 0,
|
||||||
width: video.videoWidth || 0,
|
height: video.videoHeight || 0,
|
||||||
};
|
width: video.videoWidth || 0,
|
||||||
URL.revokeObjectURL(video.src);
|
};
|
||||||
video.removeAttribute("src");
|
} finally {
|
||||||
video.load();
|
releaseVideo(video);
|
||||||
return info;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function detectMp4AlphaLayoutFromFrames(frames) {
|
export function detectMp4AlphaLayoutFromFrames(frames) {
|
||||||
@ -409,65 +411,143 @@ function roundConfidence(value) {
|
|||||||
|
|
||||||
async function readVideoFrames(file) {
|
async function readVideoFrames(file) {
|
||||||
const video = await loadVideo(file);
|
const video = await loadVideo(file);
|
||||||
const canvas = document.createElement("canvas");
|
try {
|
||||||
canvas.width = video.videoWidth;
|
const canvas = document.createElement("canvas");
|
||||||
canvas.height = video.videoHeight;
|
canvas.width = video.videoWidth;
|
||||||
const context = canvas.getContext("2d", { willReadFrequently: true });
|
canvas.height = video.videoHeight;
|
||||||
if (!context || canvas.width <= 0 || canvas.height <= 0) {
|
const context = canvas.getContext("2d", { willReadFrequently: true });
|
||||||
throw new Error("MP4 画面无法读取");
|
if (!context || canvas.width <= 0 || canvas.height <= 0) {
|
||||||
|
throw new Error("MP4 画面无法读取");
|
||||||
|
}
|
||||||
|
await waitForVideoCurrentData(video);
|
||||||
|
const duration = Number.isFinite(video.duration) && video.duration > 0 ? video.duration : 0;
|
||||||
|
const frameCount = Math.min(Math.max(Math.ceil(duration / videoFrameStepSeconds), 1), maxSeekFrameCount);
|
||||||
|
const frames = [];
|
||||||
|
for (let index = 0; index < frameCount; index += 1) {
|
||||||
|
const time = duration > 0 ? Math.min(duration - 0.001, index * videoFrameStepSeconds) : 0;
|
||||||
|
await seekVideo(video, Math.max(0, time));
|
||||||
|
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
|
frames.push({
|
||||||
|
data: context.getImageData(0, 0, canvas.width, canvas.height).data,
|
||||||
|
height: canvas.height,
|
||||||
|
width: canvas.width,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return frames;
|
||||||
|
} finally {
|
||||||
|
releaseVideo(video);
|
||||||
}
|
}
|
||||||
const duration = Number.isFinite(video.duration) && video.duration > 0 ? video.duration : 0;
|
|
||||||
const frameCount = Math.min(Math.max(Math.ceil(duration / videoFrameStepSeconds), 1), maxSeekFrameCount);
|
|
||||||
const frames = [];
|
|
||||||
for (let index = 0; index < frameCount; index += 1) {
|
|
||||||
const time = duration > 0 ? Math.min(duration - 0.001, index * videoFrameStepSeconds) : 0;
|
|
||||||
await seekVideo(video, Math.max(0, time));
|
|
||||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
||||||
frames.push({
|
|
||||||
data: context.getImageData(0, 0, canvas.width, canvas.height).data,
|
|
||||||
height: canvas.height,
|
|
||||||
width: canvas.width,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
URL.revokeObjectURL(video.src);
|
|
||||||
video.removeAttribute("src");
|
|
||||||
video.load();
|
|
||||||
return frames;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadVideo(file) {
|
function loadVideo(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
|
let settled = false;
|
||||||
|
let timeoutId = null;
|
||||||
|
const finish = (callback, value) => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
video.onloadedmetadata = null;
|
||||||
|
video.onerror = null;
|
||||||
|
callback(value);
|
||||||
|
};
|
||||||
video.muted = true;
|
video.muted = true;
|
||||||
video.playsInline = true;
|
video.playsInline = true;
|
||||||
video.preload = "metadata";
|
video.preload = "auto";
|
||||||
video.src = URL.createObjectURL(file);
|
video.src = URL.createObjectURL(file);
|
||||||
video.onloadedmetadata = () => resolve(video);
|
timeoutId = window.setTimeout(() => {
|
||||||
|
finish(reject, new Error("MP4 元数据读取超时"));
|
||||||
|
releaseVideo(video);
|
||||||
|
}, videoLoadTimeoutMs);
|
||||||
|
video.onloadedmetadata = () => finish(resolve, video);
|
||||||
video.onerror = () => {
|
video.onerror = () => {
|
||||||
URL.revokeObjectURL(video.src);
|
finish(reject, new Error("MP4 元数据读取失败"));
|
||||||
reject(new Error("MP4 元数据读取失败"));
|
releaseVideo(video);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function seekVideo(video, time) {
|
function seekVideo(video, time) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
if (Math.abs((video.currentTime || 0) - time) < 0.001 && video.readyState >= 2) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let timeoutId = null;
|
||||||
const done = () => {
|
const done = () => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
video.removeEventListener("seeked", done);
|
video.removeEventListener("seeked", done);
|
||||||
video.removeEventListener("error", fail);
|
video.removeEventListener("error", fail);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
const fail = () => {
|
const fail = () => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
video.removeEventListener("seeked", done);
|
video.removeEventListener("seeked", done);
|
||||||
video.removeEventListener("error", fail);
|
video.removeEventListener("error", fail);
|
||||||
reject(new Error("MP4 画面解析失败"));
|
reject(new Error("MP4 画面解析失败"));
|
||||||
};
|
};
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
video.removeEventListener("seeked", done);
|
||||||
|
video.removeEventListener("error", fail);
|
||||||
|
reject(new Error("MP4 画面解析超时"));
|
||||||
|
}, videoSeekTimeoutMs);
|
||||||
video.addEventListener("seeked", done, { once: true });
|
video.addEventListener("seeked", done, { once: true });
|
||||||
video.addEventListener("error", fail, { once: true });
|
video.addEventListener("error", fail, { once: true });
|
||||||
video.currentTime = time;
|
try {
|
||||||
|
video.currentTime = time;
|
||||||
|
} catch {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function waitForVideoCurrentData(video) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (video.readyState >= 2) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let timeoutId = null;
|
||||||
|
const done = () => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
video.removeEventListener("loadeddata", done);
|
||||||
|
video.removeEventListener("canplay", done);
|
||||||
|
video.removeEventListener("error", fail);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const fail = () => {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
video.removeEventListener("loadeddata", done);
|
||||||
|
video.removeEventListener("canplay", done);
|
||||||
|
video.removeEventListener("error", fail);
|
||||||
|
reject(new Error("MP4 画面读取失败"));
|
||||||
|
};
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
video.removeEventListener("loadeddata", done);
|
||||||
|
video.removeEventListener("canplay", done);
|
||||||
|
video.removeEventListener("error", fail);
|
||||||
|
reject(new Error("MP4 画面读取超时"));
|
||||||
|
}, videoLoadTimeoutMs);
|
||||||
|
video.addEventListener("loadeddata", done, { once: true });
|
||||||
|
video.addEventListener("canplay", done, { once: true });
|
||||||
|
video.addEventListener("error", fail, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseVideo(video) {
|
||||||
|
if (!video) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (video.src) {
|
||||||
|
URL.revokeObjectURL(video.src);
|
||||||
|
}
|
||||||
|
video.removeAttribute("src");
|
||||||
|
video.load();
|
||||||
|
}
|
||||||
|
|
||||||
function scoreCandidate(frames, candidate) {
|
function scoreCandidate(frames, candidate) {
|
||||||
const scores = frames.map((frame) => scoreFrameCandidate(frame, candidate));
|
const scores = frames.map((frame) => scoreFrameCandidate(frame, candidate));
|
||||||
if (!scores.length) {
|
if (!scores.length) {
|
||||||
|
|||||||
@ -710,6 +710,7 @@ function ResourceBatchUploadDialog({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<ResourceBatchPreview
|
<ResourceBatchPreview
|
||||||
|
mp4Parsing={progress.running && String(progress.label || "").includes("解析 MP4")}
|
||||||
resources={plan.resources}
|
resources={plan.resources}
|
||||||
onChangeMp4Frame={onChangeMp4Frame}
|
onChangeMp4Frame={onChangeMp4Frame}
|
||||||
onChangeMp4Layout={onChangeMp4Layout}
|
onChangeMp4Layout={onChangeMp4Layout}
|
||||||
@ -738,7 +739,7 @@ function ResourceBatchRules() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ResourceBatchPreview({ onChangeMp4Frame, onChangeMp4Layout, onConfirmMp4Layout, resources }) {
|
function ResourceBatchPreview({ mp4Parsing, onChangeMp4Frame, onChangeMp4Layout, onConfirmMp4Layout, resources }) {
|
||||||
if (!resources.length) {
|
if (!resources.length) {
|
||||||
return <div className={styles.batchEmpty}>尚未选择资源文件夹</div>;
|
return <div className={styles.batchEmpty}>尚未选择资源文件夹</div>;
|
||||||
}
|
}
|
||||||
@ -761,6 +762,7 @@ function ResourceBatchPreview({ onChangeMp4Frame, onChangeMp4Layout, onConfirmMp
|
|||||||
{resourceBatchUploadRoleLabels.cover} / {resourceBatchUploadRoleLabels.animation}
|
{resourceBatchUploadRoleLabels.cover} / {resourceBatchUploadRoleLabels.animation}
|
||||||
</span>
|
</span>
|
||||||
<BatchMp4LayoutCell
|
<BatchMp4LayoutCell
|
||||||
|
mp4Parsing={mp4Parsing}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
onChangeFrame={(frameKey, value) => onChangeMp4Frame(index, frameKey, value)}
|
onChangeFrame={(frameKey, value) => onChangeMp4Frame(index, frameKey, value)}
|
||||||
onChangeLayout={(alphaLayout) => onChangeMp4Layout(index, alphaLayout)}
|
onChangeLayout={(alphaLayout) => onChangeMp4Layout(index, alphaLayout)}
|
||||||
@ -772,13 +774,13 @@ function ResourceBatchPreview({ onChangeMp4Frame, onChangeMp4Layout, onConfirmMp
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BatchMp4LayoutCell({ onChangeFrame, onChangeLayout, onConfirm, resource }) {
|
function BatchMp4LayoutCell({ mp4Parsing, onChangeFrame, onChangeLayout, onConfirm, resource }) {
|
||||||
if (!isMp4File(resource.animationFile)) {
|
if (!isMp4File(resource.animationFile)) {
|
||||||
return <span className={styles.meta}>-</span>;
|
return <span className={styles.meta}>-</span>;
|
||||||
}
|
}
|
||||||
const layout = mp4AlphaLayoutFromMetadata(resource.metadataJson);
|
const layout = mp4AlphaLayoutFromMetadata(resource.metadataJson);
|
||||||
if (!layout) {
|
if (!layout) {
|
||||||
return <span className={styles.meta}>{resource.mp4LayoutError || "解析失败"}</span>;
|
return <span className={styles.meta}>{mp4Parsing ? "解析中" : resource.mp4LayoutError || "解析失败"}</span>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={styles.mp4BatchCell}>
|
<div className={styles.mp4BatchCell}>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user