feat: 新增导出设置、过渡与缓动及重做支持
This commit is contained in:
1 parent
482d649f84
commit
2c59709f6b
18 files changed
+1174
-351
No files matched your search
@@ -9,12 +9,10 @@ import { applyActiveCameraViewAtTime, applyTimelineTransformsAtTime } from "../r
|
||||
import { getCameraRenderQuaternion, getCameraViewSnapshotFromShot, getWorldCameraViewSnapshot, type CameraViewSnapshot } from "../schema/cameraGeometry";
|
||||
import { getViewportAspectRatioValue } from "../schema/viewportAspectRatio";
|
||||
import { getActiveCameraIdAtTime, normalizeTimelineFps } from "../schema/timeline";
|
||||
import { EXPORT_CODEC_OPTIONS, EXPORT_QUALITY_OPTIONS, type ExportCodecPreference, type ExportSettings } from "../schema/directorProject";
|
||||
|
||||
export type VideoExportView = "director" | "camera";
|
||||
|
||||
/** 导出分辨率按「短边 = 720」基准缩放,画面比例取自 viewportAspectRatio(auto 时用视口当前比例) */
|
||||
const EXPORT_SHORT_EDGE = 720;
|
||||
|
||||
function pickMimeType(): { mimeType: string; ext: string } {
|
||||
if (typeof MediaRecorder === "undefined") return { mimeType: "", ext: "webm" };
|
||||
const candidates = [
|
||||
@@ -74,34 +72,51 @@ function applyCameraSnapshotToFrameCamera(camera: PerspectiveCamera, view: Camer
|
||||
}
|
||||
|
||||
/**
|
||||
* 按视频比例(viewportAspectRatio)计算导出尺寸,短边 720,宽高取偶(编码器要求)。
|
||||
* 按视频比例(viewportAspectRatio)计算导出尺寸,短边取自导出设置(默认 720),宽高取偶(编码器要求)。
|
||||
* 「自动」也固定按 16:9 导出 —— 导出画面比例绝不受视口布局(如时间轴拖高压扁视口)影响,
|
||||
* 否则视口是扁的时导出也会跟着扁。
|
||||
*/
|
||||
export function computeExportSize(
|
||||
aspectRatioId: ReturnType<typeof useDirectorStore.getState>["viewportAspectRatio"]
|
||||
aspectRatioId: ReturnType<typeof useDirectorStore.getState>["viewportAspectRatio"],
|
||||
resolutionShortEdge: number
|
||||
): { width: number; height: number } {
|
||||
const aspect = getViewportAspectRatioValue(aspectRatioId) ?? 16 / 9;
|
||||
let width: number;
|
||||
let height: number;
|
||||
if (aspect >= 1) {
|
||||
height = EXPORT_SHORT_EDGE;
|
||||
height = resolutionShortEdge;
|
||||
width = Math.round(height * aspect);
|
||||
} else {
|
||||
width = EXPORT_SHORT_EDGE;
|
||||
width = resolutionShortEdge;
|
||||
height = Math.round(width / aspect);
|
||||
}
|
||||
return { width: width + (width % 2), height: height + (height % 2) };
|
||||
}
|
||||
|
||||
async function pickVideoEncoderConfig(width: number, height: number, fps: number) {
|
||||
const candidates = [
|
||||
{ codec: "avc1.640028" }, // High 4.0
|
||||
{ codec: "avc1.4d0028" }, // Main 4.0
|
||||
{ codec: "avc1.42001f" }, // Baseline 3.1
|
||||
];
|
||||
const bitrate = Math.round(width * height * fps * 0.15); // ≈6Mbps @720p30
|
||||
for (const { codec } of candidates) {
|
||||
/** 按画质档位算目标码率(custom 时直接用手填值)。统一 H.264,这里只影响码率大小。 */
|
||||
function computeExportBitrate(width: number, height: number, fps: number, settings: ExportSettings): number {
|
||||
if (settings.quality === "custom") return Math.max(100_000, Math.round(settings.customBitrate));
|
||||
const opt = EXPORT_QUALITY_OPTIONS.find((item) => item.value === settings.quality);
|
||||
const multiplier = opt?.bitrateMultiplier ?? 0.15;
|
||||
return Math.round(width * height * fps * multiplier);
|
||||
}
|
||||
|
||||
/** 编码档位 → 实际要尝试的 H.264 codec 串列表(auto 时从高规格往下兜底) */
|
||||
function buildCodecCandidates(pref: ExportCodecPreference): string[] {
|
||||
const chosen = EXPORT_CODEC_OPTIONS.find((item) => item.value === pref);
|
||||
if (chosen?.codec) return [chosen.codec];
|
||||
return EXPORT_CODEC_OPTIONS.filter((item) => item.codec).map((item) => item.codec as string);
|
||||
}
|
||||
|
||||
async function pickVideoEncoderConfig(
|
||||
width: number,
|
||||
height: number,
|
||||
fps: number,
|
||||
settings: ExportSettings
|
||||
): Promise<VideoEncoderConfig | null> {
|
||||
const bitrate = computeExportBitrate(width, height, fps, settings);
|
||||
const candidates = buildCodecCandidates(settings.codec);
|
||||
for (const codec of candidates) {
|
||||
const config: VideoEncoderConfig = {
|
||||
codec,
|
||||
width,
|
||||
@@ -121,7 +136,8 @@ async function pickVideoEncoderConfig(width: number, height: number, fps: number
|
||||
|
||||
/**
|
||||
* 逐帧离线导出(快,不用实时播放一遍):
|
||||
* - 渲染分辨率固定为视频比例(短边 720),不受视口布局影响 —— 时间轴拖多高画面都不会变扁
|
||||
* - 渲染分辨率 / 码率 / 编码档位取自工程里的导出设置,且固定为视频比例,不受视口布局影响
|
||||
* —— 时间轴拖多高画面都不会变扁
|
||||
* - 每帧直接 setPlayhead(t) + 手动渲染 + WebCodecs 编码,渲染多快导多快
|
||||
* - H.264 编码进 MP4(Chrome / Electron 均支持)
|
||||
*/
|
||||
@@ -132,13 +148,14 @@ async function exportVideoFrameByFrame(view: VideoExportView): Promise<void> {
|
||||
const canvas = gl.domElement;
|
||||
|
||||
const directorStore = useDirectorStore.getState();
|
||||
const { width, height } = computeExportSize(directorStore.viewportAspectRatio);
|
||||
const exportSettings = directorStore.project.exportSettings;
|
||||
const { width, height } = computeExportSize(directorStore.viewportAspectRatio, exportSettings.resolutionShortEdge);
|
||||
const duration = directorStore.project.timeline.duration;
|
||||
// 帧率取自工程设置(时间轴设置里的「每秒帧数」),导出与读数保持一致
|
||||
const fps = normalizeTimelineFps(directorStore.project.timeline.fps);
|
||||
const totalFrames = Math.max(1, Math.round(duration * fps));
|
||||
|
||||
const encoderConfig = await pickVideoEncoderConfig(width, height, fps);
|
||||
const encoderConfig = await pickVideoEncoderConfig(width, height, fps, exportSettings);
|
||||
if (!encoderConfig) throw new Error("当前浏览器不支持 H.264 编码,请用最新版 Chrome / Edge");
|
||||
|
||||
// 保存现场:导出完原样恢复(视角 / 指针 / 渲染尺寸 / 相机比例)。
|
||||
@@ -227,6 +244,8 @@ async function exportVideoFrameByFrame(view: VideoExportView): Promise<void> {
|
||||
playhead: time,
|
||||
viewMode: view,
|
||||
editingObjectId: usePlaybackStore.getState().editingObjectId,
|
||||
transition: directorStore.project.timeline.cameraTransition,
|
||||
fallbackCameraId: directorStore.project.activeCameraId,
|
||||
});
|
||||
|
||||
// 没覆盖 = 这个机位没有运镜轨道。此时必须把机位自己的视角写进导出相机,
|
||||
|
||||
Reference in new issue
Block a user