@codingfactory/mediables-vue 2.7.2 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{PixiFrameExporter-n220y4aZ.cjs → PixiFrameExporter-COcgeYmj.cjs} +2 -2
- package/dist/{PixiFrameExporter-n220y4aZ.cjs.map → PixiFrameExporter-COcgeYmj.cjs.map} +1 -1
- package/dist/{PixiFrameExporter-C5RSaXvT.js → PixiFrameExporter-SiG3q5-_.js} +2 -2
- package/dist/{PixiFrameExporter-C5RSaXvT.js.map → PixiFrameExporter-SiG3q5-_.js.map} +1 -1
- package/dist/components/video/ui-v2/AssetRail.vue.d.ts +2 -0
- package/dist/components/video/ui-v2/AssetRailItem.vue.d.ts +7 -0
- package/dist/components/video/ui-v2/AudioMixerPanel.vue.d.ts +33 -0
- package/dist/components/video/ui-v2/CaptionSrtPanel.vue.d.ts +9 -0
- package/dist/components/video/ui-v2/DesktopInspectorSections.vue.d.ts +21 -0
- package/dist/components/video/ui-v2/DraftRecoveryBanner.vue.d.ts +12 -0
- package/dist/components/video/ui-v2/KeyframePresetPanel.vue.d.ts +26 -0
- package/dist/components/video/ui-v2/MobileClipSummary.vue.d.ts +4 -0
- package/dist/components/video/ui-v2/MobileToolPicker.vue.d.ts +3 -0
- package/dist/components/video/ui-v2/SplitPanelLayout.vue.d.ts +9 -5
- package/dist/components/video/ui-v2/TimelineTrackHeader.vue.d.ts +2 -0
- package/dist/composables/useClientVideoExport.d.ts +27 -0
- package/dist/composables/useLiveStream.d.ts +2 -2
- package/dist/composables/useRadialMenu.d.ts +1 -1
- package/dist/composables/useVideoEditor.d.ts +119 -7
- package/dist/composables/useVideoFilters.d.ts +4 -4
- package/dist/index-B6oyn6Pa.cjs +350 -0
- package/dist/index-B6oyn6Pa.cjs.map +1 -0
- package/dist/index-BGexNz7s.js +32993 -0
- package/dist/index-BGexNz7s.js.map +1 -0
- package/dist/mediables-vue.cjs +1 -1
- package/dist/mediables-vue.mjs +1 -1
- package/dist/render-page/assets/{index-aDRQNAjC.js → index-jZGmiMRr.js} +972 -159
- package/dist/render-page/index.html +1 -1
- package/dist/services/VideoJobClient.d.ts +1 -1
- package/dist/style.css +1 -1
- package/dist/types/api.d.ts +14 -0
- package/dist/types/video.d.ts +164 -5
- package/dist/video/project/audioMixerSchema.d.ts +152 -0
- package/dist/video/project/captionSrt.d.ts +23 -0
- package/dist/video/project/draftRecovery.d.ts +86 -0
- package/dist/video/project/exportPresets.d.ts +172 -0
- package/dist/video/project/index.d.ts +22 -0
- package/dist/video/project/keyframeAutomation.d.ts +91 -0
- package/dist/video/project/mediaSourceAnalysis.d.ts +127 -0
- package/dist/video/project/mediaSourceCache.d.ts +47 -0
- package/dist/video/project/recipeMigration.d.ts +26 -0
- package/dist/video/project/timelineSelection.d.ts +23 -0
- package/dist/video/project/timelineTransactions.d.ts +129 -0
- package/dist/video/project/visualLayerSchema.d.ts +238 -0
- package/dist/video-engine/adapters/AudioManager.d.ts +9 -0
- package/dist/video-engine/adapters/MediablesCompositionAdapter.d.ts +3 -0
- package/dist/video-engine/adapters/TextOverlayManager.d.ts +4 -2
- package/package.json +1 -1
- package/dist/index-B_7DfcKr.js +0 -29339
- package/dist/index-B_7DfcKr.js.map +0 -1
- package/dist/index-Dx7DOxwK.cjs +0 -342
- package/dist/index-Dx7DOxwK.cjs.map +0 -1
|
@@ -71050,6 +71050,548 @@ function resetTime(video) {
|
|
|
71050
71050
|
} catch {
|
|
71051
71051
|
}
|
|
71052
71052
|
}
|
|
71053
|
+
const MIN_PLAYBACK_RATE = 0.1;
|
|
71054
|
+
const MAX_PLAYBACK_RATE = 16;
|
|
71055
|
+
const DEFAULT_PLAYBACK_RATE = 1;
|
|
71056
|
+
const DEFAULT_FRAME_TIMEOUT_MS = 4e3;
|
|
71057
|
+
class PlaybackCaptureFallbackError extends Error {
|
|
71058
|
+
constructor(startFrame, reason) {
|
|
71059
|
+
super(reason);
|
|
71060
|
+
this.name = "PlaybackCaptureFallbackError";
|
|
71061
|
+
this.startFrame = startFrame;
|
|
71062
|
+
}
|
|
71063
|
+
}
|
|
71064
|
+
function hasRequestVideoFrameCallback(video) {
|
|
71065
|
+
return typeof video.requestVideoFrameCallback === "function";
|
|
71066
|
+
}
|
|
71067
|
+
function normalizeRenderCaptureMode(value) {
|
|
71068
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
71069
|
+
if (normalized === "seek" || normalized === "rvfc" || normalized === "auto") {
|
|
71070
|
+
return normalized;
|
|
71071
|
+
}
|
|
71072
|
+
return "auto";
|
|
71073
|
+
}
|
|
71074
|
+
function normalizeCapturePlaybackRate(value) {
|
|
71075
|
+
if (!Number.isFinite(value) || value === void 0 || value <= 0) {
|
|
71076
|
+
return DEFAULT_PLAYBACK_RATE;
|
|
71077
|
+
}
|
|
71078
|
+
return Math.min(MAX_PLAYBACK_RATE, Math.max(MIN_PLAYBACK_RATE, value));
|
|
71079
|
+
}
|
|
71080
|
+
function normalizeMaxFrameGap(value) {
|
|
71081
|
+
if (!Number.isFinite(value) || value === void 0 || value < 0) {
|
|
71082
|
+
return 0;
|
|
71083
|
+
}
|
|
71084
|
+
return Math.floor(value);
|
|
71085
|
+
}
|
|
71086
|
+
function normalizeFrameTimeoutMs(value) {
|
|
71087
|
+
if (!Number.isFinite(value) || value === void 0 || value < 250) {
|
|
71088
|
+
return DEFAULT_FRAME_TIMEOUT_MS;
|
|
71089
|
+
}
|
|
71090
|
+
return Math.floor(value);
|
|
71091
|
+
}
|
|
71092
|
+
function hasUnsupportedTimeline(recipe) {
|
|
71093
|
+
const clips = Array.isArray(recipe.timeline) ? recipe.timeline : [];
|
|
71094
|
+
if (clips.length > 1) {
|
|
71095
|
+
return true;
|
|
71096
|
+
}
|
|
71097
|
+
const firstClip = clips[0];
|
|
71098
|
+
if (firstClip === void 0) {
|
|
71099
|
+
return false;
|
|
71100
|
+
}
|
|
71101
|
+
return typeof firstClip.speed === "number" && Number.isFinite(firstClip.speed) && Math.abs(firstClip.speed - 1) > 1e-4;
|
|
71102
|
+
}
|
|
71103
|
+
function resolveRenderCaptureMode(options) {
|
|
71104
|
+
const requestedMode = normalizeRenderCaptureMode(options.requestedMode);
|
|
71105
|
+
if (requestedMode === "seek") {
|
|
71106
|
+
return {
|
|
71107
|
+
requestedMode,
|
|
71108
|
+
effectiveMode: "seek",
|
|
71109
|
+
fallbackReason: null
|
|
71110
|
+
};
|
|
71111
|
+
}
|
|
71112
|
+
if (!options.supportsRequestVideoFrameCallback) {
|
|
71113
|
+
if (requestedMode === "rvfc") {
|
|
71114
|
+
return {
|
|
71115
|
+
requestedMode,
|
|
71116
|
+
effectiveMode: "seek",
|
|
71117
|
+
fallbackReason: "requestVideoFrameCallback unavailable"
|
|
71118
|
+
};
|
|
71119
|
+
}
|
|
71120
|
+
return {
|
|
71121
|
+
requestedMode,
|
|
71122
|
+
effectiveMode: "seek",
|
|
71123
|
+
fallbackReason: "auto selected seek because requestVideoFrameCallback is unavailable"
|
|
71124
|
+
};
|
|
71125
|
+
}
|
|
71126
|
+
if (!Number.isFinite(options.fps) || options.fps <= 0) {
|
|
71127
|
+
return {
|
|
71128
|
+
requestedMode,
|
|
71129
|
+
effectiveMode: "seek",
|
|
71130
|
+
fallbackReason: "auto selected seek because fps is invalid"
|
|
71131
|
+
};
|
|
71132
|
+
}
|
|
71133
|
+
if (!Number.isFinite(options.trimStart) || !Number.isFinite(options.trimEnd) || options.trimEnd <= options.trimStart) {
|
|
71134
|
+
return {
|
|
71135
|
+
requestedMode,
|
|
71136
|
+
effectiveMode: "seek",
|
|
71137
|
+
fallbackReason: "auto selected seek because trim bounds are invalid"
|
|
71138
|
+
};
|
|
71139
|
+
}
|
|
71140
|
+
if (hasUnsupportedTimeline(options.recipe)) {
|
|
71141
|
+
return {
|
|
71142
|
+
requestedMode,
|
|
71143
|
+
effectiveMode: "seek",
|
|
71144
|
+
fallbackReason: "auto selected seek because the recipe is not a single forward clip"
|
|
71145
|
+
};
|
|
71146
|
+
}
|
|
71147
|
+
return {
|
|
71148
|
+
requestedMode,
|
|
71149
|
+
effectiveMode: "rvfc",
|
|
71150
|
+
fallbackReason: null
|
|
71151
|
+
};
|
|
71152
|
+
}
|
|
71153
|
+
function mapMediaTimeToOutputFrame(mediaTime, trimStart, fps) {
|
|
71154
|
+
if (!Number.isFinite(mediaTime) || !Number.isFinite(trimStart) || !Number.isFinite(fps) || fps <= 0) {
|
|
71155
|
+
return 0;
|
|
71156
|
+
}
|
|
71157
|
+
return Math.max(0, Math.round((mediaTime - trimStart) * fps));
|
|
71158
|
+
}
|
|
71159
|
+
function outputTimeForFrame(frame, trimStart, fps) {
|
|
71160
|
+
return trimStart + frame / fps;
|
|
71161
|
+
}
|
|
71162
|
+
async function waitForNextVideoFrame(video, timeoutMs) {
|
|
71163
|
+
return new Promise((resolve, reject) => {
|
|
71164
|
+
let settled = false;
|
|
71165
|
+
let callbackHandle = null;
|
|
71166
|
+
const finish = (metadata, error) => {
|
|
71167
|
+
if (settled) {
|
|
71168
|
+
return;
|
|
71169
|
+
}
|
|
71170
|
+
settled = true;
|
|
71171
|
+
window.clearTimeout(timeoutHandle);
|
|
71172
|
+
if (callbackHandle !== null && typeof video.cancelVideoFrameCallback === "function") {
|
|
71173
|
+
video.cancelVideoFrameCallback(callbackHandle);
|
|
71174
|
+
}
|
|
71175
|
+
if (error !== void 0) {
|
|
71176
|
+
reject(error);
|
|
71177
|
+
return;
|
|
71178
|
+
}
|
|
71179
|
+
if (metadata === null) {
|
|
71180
|
+
reject(new Error("Video frame callback completed without metadata"));
|
|
71181
|
+
return;
|
|
71182
|
+
}
|
|
71183
|
+
resolve(metadata);
|
|
71184
|
+
};
|
|
71185
|
+
const timeoutHandle = window.setTimeout(() => {
|
|
71186
|
+
finish(null, new Error(`Timed out waiting for playback frame after ${timeoutMs}ms`));
|
|
71187
|
+
}, timeoutMs);
|
|
71188
|
+
callbackHandle = video.requestVideoFrameCallback((_now, metadata) => {
|
|
71189
|
+
finish(metadata);
|
|
71190
|
+
});
|
|
71191
|
+
});
|
|
71192
|
+
}
|
|
71193
|
+
async function runSeekCaptureLoop(options) {
|
|
71194
|
+
const startFrame = Math.max(0, Math.floor(options.startFrame ?? 0));
|
|
71195
|
+
for (let frame = startFrame; frame < options.totalFrames; frame++) {
|
|
71196
|
+
const timeSec = outputTimeForFrame(frame, options.trimStart, options.fps);
|
|
71197
|
+
options.touchHeartbeat("capturing-seek");
|
|
71198
|
+
await options.seekToFrame(frame, timeSec);
|
|
71199
|
+
await options.captureFrame({
|
|
71200
|
+
frame,
|
|
71201
|
+
timeSec,
|
|
71202
|
+
duplicatePreviousFrame: false
|
|
71203
|
+
});
|
|
71204
|
+
options.updateProgress(frame);
|
|
71205
|
+
options.touchHeartbeat("encoding-video");
|
|
71206
|
+
}
|
|
71207
|
+
return options.totalFrames;
|
|
71208
|
+
}
|
|
71209
|
+
async function runPlaybackCaptureLoop(options) {
|
|
71210
|
+
var _a;
|
|
71211
|
+
if (!hasRequestVideoFrameCallback(options.video)) {
|
|
71212
|
+
throw new PlaybackCaptureFallbackError(
|
|
71213
|
+
Math.max(0, Math.floor(options.startFrame ?? 0)),
|
|
71214
|
+
"requestVideoFrameCallback unavailable"
|
|
71215
|
+
);
|
|
71216
|
+
}
|
|
71217
|
+
const video = options.video;
|
|
71218
|
+
let nextFrame = Math.max(0, Math.floor(options.startFrame ?? 0));
|
|
71219
|
+
const previousPlaybackRate = video.playbackRate;
|
|
71220
|
+
const previousMuted = video.muted;
|
|
71221
|
+
const timeoutMs = normalizeFrameTimeoutMs(options.frameTimeoutMs);
|
|
71222
|
+
try {
|
|
71223
|
+
video.muted = true;
|
|
71224
|
+
video.playbackRate = normalizeCapturePlaybackRate(options.playbackRate);
|
|
71225
|
+
if (nextFrame < options.totalFrames) {
|
|
71226
|
+
const firstTimeSec = outputTimeForFrame(nextFrame, options.trimStart, options.fps);
|
|
71227
|
+
options.touchHeartbeat("capturing-playback");
|
|
71228
|
+
await options.seekToFrame(nextFrame, firstTimeSec);
|
|
71229
|
+
await options.captureFrame({
|
|
71230
|
+
frame: nextFrame,
|
|
71231
|
+
timeSec: firstTimeSec,
|
|
71232
|
+
duplicatePreviousFrame: false
|
|
71233
|
+
});
|
|
71234
|
+
options.updateProgress(nextFrame);
|
|
71235
|
+
nextFrame += 1;
|
|
71236
|
+
}
|
|
71237
|
+
while (nextFrame < options.totalFrames) {
|
|
71238
|
+
options.touchHeartbeat("capturing-playback");
|
|
71239
|
+
try {
|
|
71240
|
+
await video.play();
|
|
71241
|
+
} catch (error) {
|
|
71242
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
71243
|
+
throw new PlaybackCaptureFallbackError(nextFrame, `playback start failed: ${message}`);
|
|
71244
|
+
}
|
|
71245
|
+
const waitStartedAt = performance.now();
|
|
71246
|
+
const metadata = await waitForNextVideoFrame(video, timeoutMs);
|
|
71247
|
+
(_a = options.markPlaybackWait) == null ? void 0 : _a.call(options, performance.now() - waitStartedAt);
|
|
71248
|
+
video.pause();
|
|
71249
|
+
if (metadata.mediaTime < options.trimStart) {
|
|
71250
|
+
continue;
|
|
71251
|
+
}
|
|
71252
|
+
if (metadata.mediaTime > options.trimEnd + 1 / options.fps) {
|
|
71253
|
+
throw new PlaybackCaptureFallbackError(nextFrame, "playback moved beyond trim end");
|
|
71254
|
+
}
|
|
71255
|
+
const frameIndex = mapMediaTimeToOutputFrame(metadata.mediaTime, options.trimStart, options.fps);
|
|
71256
|
+
if (frameIndex < nextFrame) {
|
|
71257
|
+
continue;
|
|
71258
|
+
}
|
|
71259
|
+
const gap = frameIndex - nextFrame;
|
|
71260
|
+
if (gap > options.maxFrameGap) {
|
|
71261
|
+
throw new PlaybackCaptureFallbackError(
|
|
71262
|
+
nextFrame,
|
|
71263
|
+
`playback skipped ${gap} output frame${gap === 1 ? "" : "s"}`
|
|
71264
|
+
);
|
|
71265
|
+
}
|
|
71266
|
+
while (nextFrame < frameIndex) {
|
|
71267
|
+
const duplicateTimeSec = outputTimeForFrame(nextFrame, options.trimStart, options.fps);
|
|
71268
|
+
await options.captureFrame({
|
|
71269
|
+
frame: nextFrame,
|
|
71270
|
+
timeSec: duplicateTimeSec,
|
|
71271
|
+
duplicatePreviousFrame: true
|
|
71272
|
+
});
|
|
71273
|
+
options.updateProgress(nextFrame);
|
|
71274
|
+
nextFrame += 1;
|
|
71275
|
+
}
|
|
71276
|
+
const timeSec = outputTimeForFrame(frameIndex, options.trimStart, options.fps);
|
|
71277
|
+
await options.captureFrame({
|
|
71278
|
+
frame: frameIndex,
|
|
71279
|
+
timeSec,
|
|
71280
|
+
duplicatePreviousFrame: false
|
|
71281
|
+
});
|
|
71282
|
+
options.updateProgress(frameIndex);
|
|
71283
|
+
nextFrame = frameIndex + 1;
|
|
71284
|
+
}
|
|
71285
|
+
return nextFrame;
|
|
71286
|
+
} finally {
|
|
71287
|
+
video.pause();
|
|
71288
|
+
video.playbackRate = previousPlaybackRate;
|
|
71289
|
+
video.muted = previousMuted;
|
|
71290
|
+
}
|
|
71291
|
+
}
|
|
71292
|
+
function nowMs() {
|
|
71293
|
+
return performance.now();
|
|
71294
|
+
}
|
|
71295
|
+
function roundMs(value) {
|
|
71296
|
+
return Math.round(value * 100) / 100;
|
|
71297
|
+
}
|
|
71298
|
+
function createRenderMetrics(options) {
|
|
71299
|
+
const startedAt = nowMs();
|
|
71300
|
+
let captureMode = options.captureMode;
|
|
71301
|
+
let fallbackReason = options.fallbackReason ?? null;
|
|
71302
|
+
let framesRendered = 0;
|
|
71303
|
+
let duplicateFrames = 0;
|
|
71304
|
+
let playbackFallbacks = 0;
|
|
71305
|
+
let seekMs = 0;
|
|
71306
|
+
let sourceSnapshotMs = 0;
|
|
71307
|
+
let textureUploadMs = 0;
|
|
71308
|
+
let pixiRenderMs = 0;
|
|
71309
|
+
let encodeMs = 0;
|
|
71310
|
+
let encodeBackpressureMs = 0;
|
|
71311
|
+
let frameReadbackMs = 0;
|
|
71312
|
+
let framePipeMs = 0;
|
|
71313
|
+
let framePipeBackpressureMs = 0;
|
|
71314
|
+
let videoFlushMs = 0;
|
|
71315
|
+
let ffmpegEncodeElapsedMs = 0;
|
|
71316
|
+
let audioEncodeMs = 0;
|
|
71317
|
+
let audioFlushMs = 0;
|
|
71318
|
+
let muxFinalizeMs = 0;
|
|
71319
|
+
let playbackWaitMs = 0;
|
|
71320
|
+
const markDuration = (metric, durationMs) => {
|
|
71321
|
+
if (!Number.isFinite(durationMs) || durationMs <= 0) {
|
|
71322
|
+
return;
|
|
71323
|
+
}
|
|
71324
|
+
switch (metric) {
|
|
71325
|
+
case "seek":
|
|
71326
|
+
seekMs += durationMs;
|
|
71327
|
+
return;
|
|
71328
|
+
case "sourceSnapshot":
|
|
71329
|
+
sourceSnapshotMs += durationMs;
|
|
71330
|
+
return;
|
|
71331
|
+
case "textureUpload":
|
|
71332
|
+
textureUploadMs += durationMs;
|
|
71333
|
+
return;
|
|
71334
|
+
case "pixiRender":
|
|
71335
|
+
pixiRenderMs += durationMs;
|
|
71336
|
+
return;
|
|
71337
|
+
case "encode":
|
|
71338
|
+
encodeMs += durationMs;
|
|
71339
|
+
return;
|
|
71340
|
+
case "encodeBackpressure":
|
|
71341
|
+
encodeBackpressureMs += durationMs;
|
|
71342
|
+
return;
|
|
71343
|
+
case "frameReadback":
|
|
71344
|
+
frameReadbackMs += durationMs;
|
|
71345
|
+
return;
|
|
71346
|
+
case "framePipe":
|
|
71347
|
+
framePipeMs += durationMs;
|
|
71348
|
+
return;
|
|
71349
|
+
case "framePipeBackpressure":
|
|
71350
|
+
framePipeBackpressureMs += durationMs;
|
|
71351
|
+
return;
|
|
71352
|
+
case "videoFlush":
|
|
71353
|
+
videoFlushMs += durationMs;
|
|
71354
|
+
return;
|
|
71355
|
+
case "ffmpegEncode":
|
|
71356
|
+
ffmpegEncodeElapsedMs += durationMs;
|
|
71357
|
+
return;
|
|
71358
|
+
case "audioEncode":
|
|
71359
|
+
audioEncodeMs += durationMs;
|
|
71360
|
+
return;
|
|
71361
|
+
case "audioFlush":
|
|
71362
|
+
audioFlushMs += durationMs;
|
|
71363
|
+
return;
|
|
71364
|
+
case "muxFinalize":
|
|
71365
|
+
muxFinalizeMs += durationMs;
|
|
71366
|
+
return;
|
|
71367
|
+
case "playbackWait":
|
|
71368
|
+
playbackWaitMs += durationMs;
|
|
71369
|
+
return;
|
|
71370
|
+
}
|
|
71371
|
+
};
|
|
71372
|
+
return {
|
|
71373
|
+
markDuration,
|
|
71374
|
+
markFrameRendered: () => {
|
|
71375
|
+
framesRendered += 1;
|
|
71376
|
+
},
|
|
71377
|
+
markDuplicateFrame: () => {
|
|
71378
|
+
duplicateFrames += 1;
|
|
71379
|
+
},
|
|
71380
|
+
markPlaybackFallback: (reason) => {
|
|
71381
|
+
playbackFallbacks += 1;
|
|
71382
|
+
fallbackReason = reason;
|
|
71383
|
+
},
|
|
71384
|
+
setCaptureMode: (nextCaptureMode, reason = null) => {
|
|
71385
|
+
captureMode = nextCaptureMode;
|
|
71386
|
+
fallbackReason = reason;
|
|
71387
|
+
},
|
|
71388
|
+
setFfmpegEncodeElapsed: (durationMs) => {
|
|
71389
|
+
if (Number.isFinite(durationMs) && durationMs >= 0) {
|
|
71390
|
+
ffmpegEncodeElapsedMs = durationMs;
|
|
71391
|
+
}
|
|
71392
|
+
},
|
|
71393
|
+
snapshot: () => ({
|
|
71394
|
+
encoderMode: options.encoderMode ?? "webcodecs",
|
|
71395
|
+
captureMode,
|
|
71396
|
+
requestedCaptureMode: options.requestedCaptureMode,
|
|
71397
|
+
fallbackReason,
|
|
71398
|
+
fps: options.fps,
|
|
71399
|
+
framesRendered,
|
|
71400
|
+
totalFrames: options.totalFrames,
|
|
71401
|
+
duplicateFrames,
|
|
71402
|
+
playbackFallbacks,
|
|
71403
|
+
seekMs: roundMs(seekMs),
|
|
71404
|
+
sourceSnapshotMs: roundMs(sourceSnapshotMs),
|
|
71405
|
+
textureUploadMs: roundMs(textureUploadMs),
|
|
71406
|
+
pixiRenderMs: roundMs(pixiRenderMs),
|
|
71407
|
+
encodeMs: roundMs(encodeMs),
|
|
71408
|
+
encodeBackpressureMs: roundMs(encodeBackpressureMs),
|
|
71409
|
+
frameReadbackMs: roundMs(frameReadbackMs),
|
|
71410
|
+
framePipeMs: roundMs(framePipeMs),
|
|
71411
|
+
framePipeBackpressureMs: roundMs(framePipeBackpressureMs),
|
|
71412
|
+
videoFlushMs: roundMs(videoFlushMs),
|
|
71413
|
+
ffmpegEncodeElapsedMs: roundMs(ffmpegEncodeElapsedMs),
|
|
71414
|
+
audioEncodeMs: roundMs(audioEncodeMs),
|
|
71415
|
+
audioFlushMs: roundMs(audioFlushMs),
|
|
71416
|
+
muxFinalizeMs: roundMs(muxFinalizeMs),
|
|
71417
|
+
playbackWaitMs: roundMs(playbackWaitMs),
|
|
71418
|
+
elapsedMs: roundMs(nowMs() - startedAt)
|
|
71419
|
+
})
|
|
71420
|
+
};
|
|
71421
|
+
}
|
|
71422
|
+
async function measureAsyncMetric(metrics, metric, callback) {
|
|
71423
|
+
const startedAt = nowMs();
|
|
71424
|
+
try {
|
|
71425
|
+
return await callback();
|
|
71426
|
+
} finally {
|
|
71427
|
+
metrics.markDuration(metric, nowMs() - startedAt);
|
|
71428
|
+
}
|
|
71429
|
+
}
|
|
71430
|
+
function measureSyncMetric(metrics, metric, callback) {
|
|
71431
|
+
const startedAt = nowMs();
|
|
71432
|
+
try {
|
|
71433
|
+
return callback();
|
|
71434
|
+
} finally {
|
|
71435
|
+
metrics.markDuration(metric, nowMs() - startedAt);
|
|
71436
|
+
}
|
|
71437
|
+
}
|
|
71438
|
+
const MAX_BUFFERED_BYTES = 64 * 1024 * 1024;
|
|
71439
|
+
const MAX_IN_FLIGHT_FRAMES = 4;
|
|
71440
|
+
const MAX_IN_FLIGHT_BYTES = 64 * 1024 * 1024;
|
|
71441
|
+
function waitForMilliseconds$1(milliseconds) {
|
|
71442
|
+
return new Promise((resolve) => {
|
|
71443
|
+
window.setTimeout(resolve, milliseconds);
|
|
71444
|
+
});
|
|
71445
|
+
}
|
|
71446
|
+
function decodeFrameSocketMessage(rawData) {
|
|
71447
|
+
if (typeof rawData !== "string") {
|
|
71448
|
+
return null;
|
|
71449
|
+
}
|
|
71450
|
+
try {
|
|
71451
|
+
const decoded = JSON.parse(rawData);
|
|
71452
|
+
if (typeof decoded !== "object" || decoded === null) {
|
|
71453
|
+
return null;
|
|
71454
|
+
}
|
|
71455
|
+
const message = decoded;
|
|
71456
|
+
if (message["type"] === "ack" && typeof message["frame"] === "number") {
|
|
71457
|
+
return { type: "ack", frame: message["frame"] };
|
|
71458
|
+
}
|
|
71459
|
+
if (message["type"] === "error" && typeof message["message"] === "string") {
|
|
71460
|
+
return { type: "error", message: message["message"] };
|
|
71461
|
+
}
|
|
71462
|
+
return null;
|
|
71463
|
+
} catch {
|
|
71464
|
+
return null;
|
|
71465
|
+
}
|
|
71466
|
+
}
|
|
71467
|
+
async function waitForSocketOpen(socket) {
|
|
71468
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
71469
|
+
return;
|
|
71470
|
+
}
|
|
71471
|
+
if (socket.readyState === WebSocket.CLOSING || socket.readyState === WebSocket.CLOSED) {
|
|
71472
|
+
throw new Error("ffmpeg frame WebSocket closed before opening");
|
|
71473
|
+
}
|
|
71474
|
+
await new Promise((resolve, reject) => {
|
|
71475
|
+
const onOpen = () => {
|
|
71476
|
+
cleanup();
|
|
71477
|
+
resolve();
|
|
71478
|
+
};
|
|
71479
|
+
const onError = () => {
|
|
71480
|
+
cleanup();
|
|
71481
|
+
reject(new Error("ffmpeg frame WebSocket failed to open"));
|
|
71482
|
+
};
|
|
71483
|
+
const cleanup = () => {
|
|
71484
|
+
socket.removeEventListener("open", onOpen);
|
|
71485
|
+
socket.removeEventListener("error", onError);
|
|
71486
|
+
};
|
|
71487
|
+
socket.addEventListener("open", onOpen);
|
|
71488
|
+
socket.addEventListener("error", onError);
|
|
71489
|
+
});
|
|
71490
|
+
}
|
|
71491
|
+
async function waitForSendWindow(socket, pendingAcks, inFlightBytes, nextFrameBytes) {
|
|
71492
|
+
while (socket.bufferedAmount > MAX_BUFFERED_BYTES || pendingAcks.size >= MAX_IN_FLIGHT_FRAMES || pendingAcks.size > 0 && inFlightBytes() + nextFrameBytes > MAX_IN_FLIGHT_BYTES) {
|
|
71493
|
+
if (socket.readyState === WebSocket.CLOSING || socket.readyState === WebSocket.CLOSED) {
|
|
71494
|
+
throw new Error("ffmpeg frame WebSocket closed while waiting for send window");
|
|
71495
|
+
}
|
|
71496
|
+
await waitForMilliseconds$1(10);
|
|
71497
|
+
}
|
|
71498
|
+
}
|
|
71499
|
+
function createFfmpegFrameSocket(url2) {
|
|
71500
|
+
const socket = new WebSocket(url2);
|
|
71501
|
+
socket.binaryType = "arraybuffer";
|
|
71502
|
+
const pendingAcks = /* @__PURE__ */ new Map();
|
|
71503
|
+
let pendingBytes = 0;
|
|
71504
|
+
const rejectPending = (error) => {
|
|
71505
|
+
for (const pendingAck of pendingAcks.values()) {
|
|
71506
|
+
pendingAck.reject(error);
|
|
71507
|
+
}
|
|
71508
|
+
pendingAcks.clear();
|
|
71509
|
+
pendingBytes = 0;
|
|
71510
|
+
};
|
|
71511
|
+
socket.addEventListener("message", (event) => {
|
|
71512
|
+
const message = decodeFrameSocketMessage(event.data);
|
|
71513
|
+
if (message === null) {
|
|
71514
|
+
return;
|
|
71515
|
+
}
|
|
71516
|
+
if (message.type === "error") {
|
|
71517
|
+
rejectPending(new Error(message.message));
|
|
71518
|
+
return;
|
|
71519
|
+
}
|
|
71520
|
+
const pendingAck = pendingAcks.get(message.frame);
|
|
71521
|
+
if (pendingAck === void 0) {
|
|
71522
|
+
rejectPending(new Error(`Unexpected ffmpeg frame ack ${message.frame}; no matching frame is pending`));
|
|
71523
|
+
return;
|
|
71524
|
+
}
|
|
71525
|
+
pendingAcks.delete(message.frame);
|
|
71526
|
+
pendingBytes = Math.max(0, pendingBytes - pendingAck.bytes);
|
|
71527
|
+
pendingAck.resolve();
|
|
71528
|
+
});
|
|
71529
|
+
socket.addEventListener("error", () => {
|
|
71530
|
+
rejectPending(new Error("ffmpeg frame WebSocket error"));
|
|
71531
|
+
});
|
|
71532
|
+
socket.addEventListener("close", () => {
|
|
71533
|
+
rejectPending(new Error("ffmpeg frame WebSocket closed"));
|
|
71534
|
+
});
|
|
71535
|
+
return {
|
|
71536
|
+
sendFrame: async (frameBytes, frame, onSent) => {
|
|
71537
|
+
if (pendingAcks.has(frame)) {
|
|
71538
|
+
throw new Error(`Cannot send duplicate ffmpeg frame ${frame}; it is still awaiting ack`);
|
|
71539
|
+
}
|
|
71540
|
+
await waitForSocketOpen(socket);
|
|
71541
|
+
await waitForSendWindow(socket, pendingAcks, () => pendingBytes, frameBytes.byteLength);
|
|
71542
|
+
let resolveAck = null;
|
|
71543
|
+
let rejectAck = null;
|
|
71544
|
+
const ackPromise = new Promise((resolve, reject) => {
|
|
71545
|
+
resolveAck = resolve;
|
|
71546
|
+
rejectAck = reject;
|
|
71547
|
+
});
|
|
71548
|
+
if (resolveAck === null || rejectAck === null) {
|
|
71549
|
+
throw new Error("Failed to initialize ffmpeg frame ack handlers");
|
|
71550
|
+
}
|
|
71551
|
+
pendingAcks.set(frame, {
|
|
71552
|
+
bytes: frameBytes.byteLength,
|
|
71553
|
+
promise: ackPromise,
|
|
71554
|
+
resolve: resolveAck,
|
|
71555
|
+
reject: rejectAck
|
|
71556
|
+
});
|
|
71557
|
+
pendingBytes += frameBytes.byteLength;
|
|
71558
|
+
socket.send(frameBytes);
|
|
71559
|
+
onSent == null ? void 0 : onSent();
|
|
71560
|
+
await ackPromise;
|
|
71561
|
+
},
|
|
71562
|
+
flush: async () => {
|
|
71563
|
+
while (pendingAcks.size > 0) {
|
|
71564
|
+
await Promise.race(Array.from(pendingAcks.values(), (pendingAck) => pendingAck.promise));
|
|
71565
|
+
}
|
|
71566
|
+
},
|
|
71567
|
+
close: () => {
|
|
71568
|
+
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
|
|
71569
|
+
socket.close();
|
|
71570
|
+
}
|
|
71571
|
+
}
|
|
71572
|
+
};
|
|
71573
|
+
}
|
|
71574
|
+
async function copyCanvasToRgba(canvas, width, height, fallbackCanvas, fallbackCtx) {
|
|
71575
|
+
const expectedBytes = width * height * 4;
|
|
71576
|
+
const videoFrame = new VideoFrame(canvas, { timestamp: 0 });
|
|
71577
|
+
try {
|
|
71578
|
+
const bytes2 = new Uint8Array(expectedBytes);
|
|
71579
|
+
await videoFrame.copyTo(bytes2, { format: "RGBA" });
|
|
71580
|
+
return bytes2;
|
|
71581
|
+
} catch (error) {
|
|
71582
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
71583
|
+
console.warn("[RenderVideo] VideoFrame.copyTo(RGBA) failed; falling back to getImageData()", message);
|
|
71584
|
+
} finally {
|
|
71585
|
+
videoFrame.close();
|
|
71586
|
+
}
|
|
71587
|
+
fallbackCtx.clearRect(0, 0, width, height);
|
|
71588
|
+
fallbackCtx.drawImage(canvas, 0, 0, width, height);
|
|
71589
|
+
const imageData = fallbackCtx.getImageData(0, 0, width, height);
|
|
71590
|
+
if (fallbackCanvas.width !== width || fallbackCanvas.height !== height) {
|
|
71591
|
+
throw new Error("RGBA fallback canvas dimensions changed during readback");
|
|
71592
|
+
}
|
|
71593
|
+
return new Uint8Array(imageData.data);
|
|
71594
|
+
}
|
|
71053
71595
|
const PENDING_SEEK_DRAIN_TIMEOUT_MS = 1e3;
|
|
71054
71596
|
function resolveFitMode(recipe) {
|
|
71055
71597
|
var _a;
|
|
@@ -71110,6 +71652,31 @@ function waitForMilliseconds(milliseconds) {
|
|
|
71110
71652
|
window.setTimeout(resolve, milliseconds);
|
|
71111
71653
|
});
|
|
71112
71654
|
}
|
|
71655
|
+
function resolveEncoderMode(config) {
|
|
71656
|
+
return config.encoder === "ffmpeg" ? "ffmpeg" : "webcodecs";
|
|
71657
|
+
}
|
|
71658
|
+
function collectRenderRuntimeInfo() {
|
|
71659
|
+
const canvas = document.createElement("canvas");
|
|
71660
|
+
const gl = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
|
|
71661
|
+
const runtimeInfo = {
|
|
71662
|
+
userAgent: navigator.userAgent,
|
|
71663
|
+
webglVendor: null,
|
|
71664
|
+
webglRenderer: null,
|
|
71665
|
+
webglUnmaskedVendor: null,
|
|
71666
|
+
webglUnmaskedRenderer: null
|
|
71667
|
+
};
|
|
71668
|
+
if (gl === null) {
|
|
71669
|
+
return runtimeInfo;
|
|
71670
|
+
}
|
|
71671
|
+
runtimeInfo.webglVendor = String(gl.getParameter(gl.VENDOR));
|
|
71672
|
+
runtimeInfo.webglRenderer = String(gl.getParameter(gl.RENDERER));
|
|
71673
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
71674
|
+
if (debugInfo !== null) {
|
|
71675
|
+
runtimeInfo.webglUnmaskedVendor = String(gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL));
|
|
71676
|
+
runtimeInfo.webglUnmaskedRenderer = String(gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL));
|
|
71677
|
+
}
|
|
71678
|
+
return runtimeInfo;
|
|
71679
|
+
}
|
|
71113
71680
|
function touchRenderHeartbeat(stage) {
|
|
71114
71681
|
window.__RENDER_HEARTBEAT__ = Date.now();
|
|
71115
71682
|
if (stage !== void 0) {
|
|
@@ -71357,6 +71924,8 @@ async function main() {
|
|
|
71357
71924
|
window.__RENDER_AUDIO_STATUS__ = null;
|
|
71358
71925
|
window.__RENDER_HEARTBEAT__ = Date.now();
|
|
71359
71926
|
window.__RENDER_STAGE__ = "initializing";
|
|
71927
|
+
window.__RENDER_METRICS__ = null;
|
|
71928
|
+
window.__RENDER_RUNTIME__ = collectRenderRuntimeInfo();
|
|
71360
71929
|
try {
|
|
71361
71930
|
const config = window.__RENDER_CONFIG__;
|
|
71362
71931
|
if (!config) throw new Error("No __RENDER_CONFIG__ found on window");
|
|
@@ -71370,8 +71939,13 @@ async function main() {
|
|
|
71370
71939
|
recipeFrameRate,
|
|
71371
71940
|
frameRateSource,
|
|
71372
71941
|
bitrate = 5e6,
|
|
71373
|
-
audioBitrate = 128e3
|
|
71942
|
+
audioBitrate = 128e3,
|
|
71943
|
+
captureMode,
|
|
71944
|
+
capturePlaybackRate,
|
|
71945
|
+
maxFrameGap,
|
|
71946
|
+
frameSocketUrl
|
|
71374
71947
|
} = config;
|
|
71948
|
+
const encoderMode = resolveEncoderMode(config);
|
|
71375
71949
|
const trimStart = resolveTrimStart(recipe);
|
|
71376
71950
|
const video = document.getElementById("source-video");
|
|
71377
71951
|
touchRenderHeartbeat("loading-source");
|
|
@@ -71384,6 +71958,24 @@ async function main() {
|
|
|
71384
71958
|
const exportDuration = Math.max(0, trimEnd - trimStart);
|
|
71385
71959
|
const totalFrames = Math.max(1, Math.ceil(exportDuration * fps));
|
|
71386
71960
|
const keyFrameIntervalFrames = Math.max(1, Math.round(fps * 2));
|
|
71961
|
+
const captureModeResolution = resolveRenderCaptureMode({
|
|
71962
|
+
requestedMode: captureMode,
|
|
71963
|
+
recipe,
|
|
71964
|
+
trimStart,
|
|
71965
|
+
trimEnd,
|
|
71966
|
+
fps,
|
|
71967
|
+
supportsRequestVideoFrameCallback: typeof video.requestVideoFrameCallback === "function"
|
|
71968
|
+
});
|
|
71969
|
+
const metrics = createRenderMetrics({
|
|
71970
|
+
fps,
|
|
71971
|
+
totalFrames,
|
|
71972
|
+
requestedCaptureMode: captureModeResolution.requestedMode,
|
|
71973
|
+
captureMode: captureModeResolution.effectiveMode,
|
|
71974
|
+
encoderMode,
|
|
71975
|
+
fallbackReason: captureModeResolution.fallbackReason
|
|
71976
|
+
});
|
|
71977
|
+
window.__RENDER_METRICS__ = metrics.snapshot();
|
|
71978
|
+
console.log("[Render] Runtime resolved", window.__RENDER_RUNTIME__);
|
|
71387
71979
|
console.log("[Render] Timing resolved", {
|
|
71388
71980
|
fps,
|
|
71389
71981
|
frameRateSource: frameRateSource ?? "unknown",
|
|
@@ -71393,11 +71985,17 @@ async function main() {
|
|
|
71393
71985
|
trimEnd,
|
|
71394
71986
|
exportDuration,
|
|
71395
71987
|
totalFrames,
|
|
71396
|
-
keyFrameIntervalFrames
|
|
71988
|
+
keyFrameIntervalFrames,
|
|
71989
|
+
encoderMode,
|
|
71990
|
+
requestedCaptureMode: captureModeResolution.requestedMode,
|
|
71991
|
+
captureMode: captureModeResolution.effectiveMode,
|
|
71992
|
+
captureFallbackReason: captureModeResolution.fallbackReason
|
|
71397
71993
|
});
|
|
71398
|
-
const hasAudioEncoder = typeof AudioEncoder !== "undefined";
|
|
71994
|
+
const hasAudioEncoder = encoderMode === "webcodecs" && typeof AudioEncoder !== "undefined";
|
|
71399
71995
|
console.log("[RenderAudio] AudioEncoder available:", hasAudioEncoder);
|
|
71400
|
-
if (
|
|
71996
|
+
if (encoderMode === "ffmpeg") {
|
|
71997
|
+
window.__RENDER_AUDIO_STATUS__ = "external:ffmpeg";
|
|
71998
|
+
} else if (!hasAudioEncoder) {
|
|
71401
71999
|
window.__RENDER_AUDIO_STATUS__ = "skipped:AudioEncoder-unavailable";
|
|
71402
72000
|
}
|
|
71403
72001
|
const audioPromise = hasAudioEncoder ? decodeAudioFromSource(sourceUrl) : Promise.resolve(null);
|
|
@@ -71463,180 +72061,395 @@ async function main() {
|
|
|
71463
72061
|
}
|
|
71464
72062
|
sprite.filters = filterInstances;
|
|
71465
72063
|
const audioBuffer = await audioPromise;
|
|
71466
|
-
const hasAudio = !!audioBuffer && audioBuffer.numberOfChannels > 0 && audioBuffer.length > 0;
|
|
72064
|
+
const hasAudio = encoderMode === "webcodecs" && !!audioBuffer && audioBuffer.numberOfChannels > 0 && audioBuffer.length > 0;
|
|
71467
72065
|
console.log("[RenderAudio] hasAudio:", hasAudio, audioBuffer ? { channels: audioBuffer.numberOfChannels, length: audioBuffer.length } : "null");
|
|
71468
|
-
if (!hasAudio && hasAudioEncoder) {
|
|
72066
|
+
if (encoderMode === "webcodecs" && !hasAudio && hasAudioEncoder) {
|
|
71469
72067
|
window.__RENDER_AUDIO_STATUS__ = "skipped:decode-failed";
|
|
71470
72068
|
}
|
|
71471
|
-
const chunkWriteQueue = createChunkWriteQueue();
|
|
71472
|
-
|
|
71473
|
-
|
|
71474
|
-
|
|
71475
|
-
|
|
71476
|
-
|
|
72069
|
+
const chunkWriteQueue = encoderMode === "webcodecs" ? createChunkWriteQueue() : null;
|
|
72070
|
+
let muxer = null;
|
|
72071
|
+
let encoder = null;
|
|
72072
|
+
let ffmpegFrameSocket = null;
|
|
72073
|
+
const pendingFfmpegFrameAcks = [];
|
|
72074
|
+
let ffmpegFrameAckFailure = null;
|
|
72075
|
+
const trackFfmpegFrameAck = (ackPromise) => {
|
|
72076
|
+
let trackedAck;
|
|
72077
|
+
trackedAck = ackPromise.catch((error) => {
|
|
72078
|
+
if (ffmpegFrameAckFailure === null) {
|
|
72079
|
+
ffmpegFrameAckFailure = error instanceof Error ? error : new Error(String(error));
|
|
71477
72080
|
}
|
|
71478
|
-
})
|
|
71479
|
-
|
|
71480
|
-
|
|
71481
|
-
|
|
71482
|
-
|
|
71483
|
-
|
|
71484
|
-
|
|
71485
|
-
fastStart: false
|
|
71486
|
-
// StreamTarget doesn't support fastStart
|
|
72081
|
+
}).finally(() => {
|
|
72082
|
+
const index = pendingFfmpegFrameAcks.indexOf(trackedAck);
|
|
72083
|
+
if (index >= 0) {
|
|
72084
|
+
pendingFfmpegFrameAcks.splice(index, 1);
|
|
72085
|
+
}
|
|
72086
|
+
});
|
|
72087
|
+
pendingFfmpegFrameAcks.push(trackedAck);
|
|
71487
72088
|
};
|
|
71488
|
-
|
|
71489
|
-
|
|
71490
|
-
|
|
71491
|
-
|
|
71492
|
-
|
|
71493
|
-
|
|
71494
|
-
|
|
71495
|
-
|
|
71496
|
-
|
|
71497
|
-
|
|
71498
|
-
|
|
71499
|
-
|
|
71500
|
-
|
|
71501
|
-
|
|
71502
|
-
|
|
71503
|
-
|
|
71504
|
-
|
|
71505
|
-
|
|
71506
|
-
|
|
71507
|
-
|
|
71508
|
-
|
|
71509
|
-
|
|
71510
|
-
|
|
71511
|
-
|
|
71512
|
-
|
|
72089
|
+
const assertNoFfmpegFrameAckFailure = () => {
|
|
72090
|
+
if (ffmpegFrameAckFailure !== null) {
|
|
72091
|
+
throw ffmpegFrameAckFailure;
|
|
72092
|
+
}
|
|
72093
|
+
};
|
|
72094
|
+
const flushFfmpegFrameAcks = async () => {
|
|
72095
|
+
if (ffmpegFrameSocket !== null) {
|
|
72096
|
+
await ffmpegFrameSocket.flush();
|
|
72097
|
+
}
|
|
72098
|
+
while (pendingFfmpegFrameAcks.length > 0) {
|
|
72099
|
+
const pendingAcks = pendingFfmpegFrameAcks.splice(0);
|
|
72100
|
+
await Promise.all(pendingAcks);
|
|
72101
|
+
assertNoFfmpegFrameAckFailure();
|
|
72102
|
+
}
|
|
72103
|
+
assertNoFfmpegFrameAckFailure();
|
|
72104
|
+
};
|
|
72105
|
+
const encoderFrameRate = Math.max(1, Math.round(fps));
|
|
72106
|
+
if (encoderMode === "ffmpeg") {
|
|
72107
|
+
if (typeof frameSocketUrl !== "string" || frameSocketUrl.trim() === "") {
|
|
72108
|
+
throw new Error("ffmpeg encoder mode requires frameSocketUrl");
|
|
72109
|
+
}
|
|
72110
|
+
ffmpegFrameSocket = createFfmpegFrameSocket(frameSocketUrl);
|
|
72111
|
+
console.log("[RenderVideo] Using ffmpeg frame socket encoder");
|
|
72112
|
+
} else {
|
|
72113
|
+
const muxerOptions = {
|
|
72114
|
+
target: new StreamTarget({
|
|
72115
|
+
onData: (data, position2) => {
|
|
72116
|
+
if (chunkWriteQueue === null) {
|
|
72117
|
+
throw new Error("Chunk write queue unavailable for WebCodecs encoder");
|
|
72118
|
+
}
|
|
72119
|
+
const b64 = toBase64(data);
|
|
72120
|
+
chunkWriteQueue.enqueue(() => window.__writeChunk(b64, position2));
|
|
72121
|
+
}
|
|
72122
|
+
}),
|
|
72123
|
+
video: {
|
|
72124
|
+
codec: "avc",
|
|
72125
|
+
width,
|
|
72126
|
+
height,
|
|
72127
|
+
frameRate: encoderFrameRate
|
|
72128
|
+
},
|
|
72129
|
+
fastStart: false
|
|
72130
|
+
// StreamTarget doesn't support fastStart
|
|
71513
72131
|
};
|
|
71514
|
-
|
|
71515
|
-
|
|
71516
|
-
|
|
71517
|
-
|
|
72132
|
+
if (hasAudio && audioBuffer !== null) {
|
|
72133
|
+
muxerOptions.audio = {
|
|
72134
|
+
codec: "aac",
|
|
72135
|
+
numberOfChannels: audioBuffer.numberOfChannels,
|
|
72136
|
+
sampleRate: audioBuffer.sampleRate
|
|
72137
|
+
};
|
|
71518
72138
|
}
|
|
71519
|
-
|
|
71520
|
-
|
|
71521
|
-
|
|
71522
|
-
|
|
71523
|
-
|
|
71524
|
-
|
|
71525
|
-
|
|
71526
|
-
|
|
71527
|
-
|
|
71528
|
-
|
|
72139
|
+
muxer = new Muxer(muxerOptions);
|
|
72140
|
+
const codecCandidates = [
|
|
72141
|
+
{ codec: "avc1.640028", hw: "prefer-software" },
|
|
72142
|
+
// High Profile Level 4.0 (prefer software in headless)
|
|
72143
|
+
{ codec: "avc1.4d0028", hw: "prefer-software" },
|
|
72144
|
+
// Main Profile Level 4.0
|
|
72145
|
+
{ codec: "avc1.420028", hw: "prefer-software" }
|
|
72146
|
+
// Baseline Profile Level 4.0
|
|
72147
|
+
];
|
|
72148
|
+
let encoderConfig = null;
|
|
72149
|
+
for (const candidate of codecCandidates) {
|
|
72150
|
+
const cfg = {
|
|
72151
|
+
codec: candidate.codec,
|
|
72152
|
+
width,
|
|
72153
|
+
height,
|
|
72154
|
+
bitrate,
|
|
72155
|
+
framerate: encoderFrameRate,
|
|
72156
|
+
hardwareAcceleration: candidate.hw
|
|
72157
|
+
};
|
|
72158
|
+
const support = await VideoEncoder.isConfigSupported(cfg);
|
|
72159
|
+
if (support.supported) {
|
|
72160
|
+
encoderConfig = cfg;
|
|
72161
|
+
break;
|
|
72162
|
+
}
|
|
71529
72163
|
}
|
|
71530
|
-
|
|
71531
|
-
|
|
71532
|
-
touchRenderHeartbeat("priming-first-frame");
|
|
71533
|
-
await seekVideo(
|
|
71534
|
-
video,
|
|
71535
|
-
trimStart,
|
|
71536
|
-
config.seekTimeoutMs ?? 4e3,
|
|
71537
|
-
config.seekRetryAttempts ?? 3,
|
|
71538
|
-
fps
|
|
71539
|
-
);
|
|
71540
|
-
for (let frame = 0; frame < totalFrames; frame++) {
|
|
71541
|
-
const timeSec = trimStart + frame / fps;
|
|
71542
|
-
await seekVideo(
|
|
71543
|
-
video,
|
|
71544
|
-
timeSec,
|
|
71545
|
-
config.seekTimeoutMs ?? 4e3,
|
|
71546
|
-
config.seekRetryAttempts ?? 3,
|
|
71547
|
-
fps
|
|
71548
|
-
);
|
|
71549
|
-
await drawCurrentVideoFrameToCanvas(video, sourceCtx, sourceWidth, sourceHeight);
|
|
71550
|
-
videoTexture.source.update();
|
|
71551
|
-
app.render();
|
|
71552
|
-
const timestamp = Math.round(frame / fps * 1e6);
|
|
71553
|
-
const nextTimestamp = Math.round((frame + 1) / fps * 1e6);
|
|
71554
|
-
const frameDuration = Math.max(1, nextTimestamp - timestamp);
|
|
71555
|
-
const videoFrame = new VideoFrame(
|
|
71556
|
-
app.canvas,
|
|
71557
|
-
{ timestamp, duration: frameDuration }
|
|
71558
|
-
);
|
|
71559
|
-
const keyFrame = frame % keyFrameIntervalFrames === 0;
|
|
71560
|
-
encoder.encode(videoFrame, { keyFrame });
|
|
71561
|
-
videoFrame.close();
|
|
71562
|
-
if (encoder.encodeQueueSize > 5) {
|
|
71563
|
-
await new Promise((resolve) => {
|
|
71564
|
-
encoder.addEventListener("dequeue", () => resolve(), { once: true });
|
|
71565
|
-
});
|
|
72164
|
+
if (!encoderConfig) {
|
|
72165
|
+
throw new Error(`No supported VideoEncoder codec found for ${width}x${height}`);
|
|
71566
72166
|
}
|
|
72167
|
+
encoder = new VideoEncoder({
|
|
72168
|
+
output: (chunk, metadata) => {
|
|
72169
|
+
if (muxer === null) {
|
|
72170
|
+
throw new Error("Muxer unavailable for VideoEncoder output");
|
|
72171
|
+
}
|
|
72172
|
+
muxer.addVideoChunk(chunk, metadata);
|
|
72173
|
+
},
|
|
72174
|
+
error: (err) => {
|
|
72175
|
+
throw new Error(`VideoEncoder error: ${err.message}`);
|
|
72176
|
+
}
|
|
72177
|
+
});
|
|
72178
|
+
encoder.configure(encoderConfig);
|
|
72179
|
+
}
|
|
72180
|
+
const rgbaFallbackCanvas = document.createElement("canvas");
|
|
72181
|
+
rgbaFallbackCanvas.width = width;
|
|
72182
|
+
rgbaFallbackCanvas.height = height;
|
|
72183
|
+
const rgbaFallbackCtx = rgbaFallbackCanvas.getContext("2d", { willReadFrequently: true });
|
|
72184
|
+
if (!rgbaFallbackCtx) throw new Error("Failed to create 2D context for ffmpeg frame readback fallback");
|
|
72185
|
+
const seekTimeoutMs = config.seekTimeoutMs ?? 4e3;
|
|
72186
|
+
const seekRetryAttempts = config.seekRetryAttempts ?? 3;
|
|
72187
|
+
const updateVideoProgress = (frame) => {
|
|
71567
72188
|
window.__RENDER_PROGRESS__ = Math.round((frame + 1) / totalFrames * 90);
|
|
71568
|
-
|
|
71569
|
-
}
|
|
71570
|
-
|
|
71571
|
-
|
|
71572
|
-
|
|
71573
|
-
|
|
71574
|
-
|
|
71575
|
-
|
|
71576
|
-
|
|
71577
|
-
|
|
71578
|
-
|
|
71579
|
-
|
|
71580
|
-
|
|
71581
|
-
|
|
71582
|
-
}
|
|
71583
|
-
|
|
71584
|
-
|
|
71585
|
-
|
|
71586
|
-
|
|
71587
|
-
|
|
71588
|
-
|
|
71589
|
-
|
|
71590
|
-
|
|
71591
|
-
|
|
72189
|
+
window.__RENDER_METRICS__ = metrics.snapshot();
|
|
72190
|
+
};
|
|
72191
|
+
const seekToFrame = async (_frame, timeSec) => {
|
|
72192
|
+
const seekHeartbeat = setInterval(() => {
|
|
72193
|
+
touchRenderHeartbeat("seeking");
|
|
72194
|
+
}, 5e3);
|
|
72195
|
+
try {
|
|
72196
|
+
await measureAsyncMetric(metrics, "seek", () => seekVideo(
|
|
72197
|
+
video,
|
|
72198
|
+
timeSec,
|
|
72199
|
+
seekTimeoutMs,
|
|
72200
|
+
seekRetryAttempts,
|
|
72201
|
+
fps
|
|
72202
|
+
));
|
|
72203
|
+
} finally {
|
|
72204
|
+
clearInterval(seekHeartbeat);
|
|
72205
|
+
}
|
|
72206
|
+
};
|
|
72207
|
+
let lastRgbaFrame = null;
|
|
72208
|
+
const captureFrame = async ({
|
|
72209
|
+
frame,
|
|
72210
|
+
duplicatePreviousFrame
|
|
72211
|
+
}) => {
|
|
72212
|
+
assertNoFfmpegFrameAckFailure();
|
|
72213
|
+
if (!duplicatePreviousFrame) {
|
|
72214
|
+
await measureAsyncMetric(metrics, "sourceSnapshot", () => drawCurrentVideoFrameToCanvas(video, sourceCtx, sourceWidth, sourceHeight));
|
|
72215
|
+
measureSyncMetric(metrics, "textureUpload", () => {
|
|
72216
|
+
videoTexture.source.update();
|
|
72217
|
+
});
|
|
72218
|
+
measureSyncMetric(metrics, "pixiRender", () => {
|
|
72219
|
+
app.render();
|
|
72220
|
+
});
|
|
72221
|
+
} else {
|
|
72222
|
+
metrics.markDuplicateFrame();
|
|
72223
|
+
}
|
|
72224
|
+
const activeFrameSocket = ffmpegFrameSocket;
|
|
72225
|
+
if (activeFrameSocket !== null) {
|
|
72226
|
+
let rgbaFrame;
|
|
72227
|
+
if (duplicatePreviousFrame) {
|
|
72228
|
+
if (lastRgbaFrame === null) {
|
|
72229
|
+
throw new Error("Cannot duplicate ffmpeg frame before a previous frame exists");
|
|
71592
72230
|
}
|
|
72231
|
+
rgbaFrame = lastRgbaFrame;
|
|
72232
|
+
} else {
|
|
72233
|
+
touchRenderHeartbeat("reading-frame");
|
|
72234
|
+
rgbaFrame = await measureAsyncMetric(metrics, "frameReadback", () => copyCanvasToRgba(
|
|
72235
|
+
app.canvas,
|
|
72236
|
+
width,
|
|
72237
|
+
height,
|
|
72238
|
+
rgbaFallbackCanvas,
|
|
72239
|
+
rgbaFallbackCtx
|
|
72240
|
+
));
|
|
72241
|
+
lastRgbaFrame = rgbaFrame;
|
|
72242
|
+
}
|
|
72243
|
+
touchRenderHeartbeat("piping-frame");
|
|
72244
|
+
let markFrameSent = null;
|
|
72245
|
+
const frameSentPromise = new Promise((resolve) => {
|
|
72246
|
+
markFrameSent = resolve;
|
|
71593
72247
|
});
|
|
71594
|
-
|
|
71595
|
-
|
|
71596
|
-
|
|
71597
|
-
|
|
71598
|
-
|
|
71599
|
-
|
|
71600
|
-
|
|
71601
|
-
|
|
71602
|
-
|
|
71603
|
-
|
|
71604
|
-
|
|
72248
|
+
const frameAckPromise = activeFrameSocket.sendFrame(rgbaFrame, frame, () => {
|
|
72249
|
+
markFrameSent == null ? void 0 : markFrameSent();
|
|
72250
|
+
}).catch((error) => {
|
|
72251
|
+
markFrameSent == null ? void 0 : markFrameSent();
|
|
72252
|
+
throw error;
|
|
72253
|
+
});
|
|
72254
|
+
trackFfmpegFrameAck(frameAckPromise);
|
|
72255
|
+
await measureAsyncMetric(metrics, "framePipe", () => frameSentPromise);
|
|
72256
|
+
} else {
|
|
72257
|
+
if (encoder === null) {
|
|
72258
|
+
throw new Error("VideoEncoder unavailable for WebCodecs render path");
|
|
72259
|
+
}
|
|
72260
|
+
measureSyncMetric(metrics, "encode", () => {
|
|
72261
|
+
const timestamp = Math.round(frame / fps * 1e6);
|
|
72262
|
+
const nextTimestamp = Math.round((frame + 1) / fps * 1e6);
|
|
72263
|
+
const frameDuration = Math.max(1, nextTimestamp - timestamp);
|
|
72264
|
+
const videoFrame = new VideoFrame(
|
|
72265
|
+
app.canvas,
|
|
72266
|
+
{ timestamp, duration: frameDuration }
|
|
72267
|
+
);
|
|
72268
|
+
const keyFrame = frame % keyFrameIntervalFrames === 0;
|
|
72269
|
+
encoder.encode(videoFrame, { keyFrame });
|
|
72270
|
+
videoFrame.close();
|
|
72271
|
+
});
|
|
72272
|
+
if (encoder.encodeQueueSize > 5) {
|
|
72273
|
+
await measureAsyncMetric(metrics, "encodeBackpressure", () => new Promise((resolve) => {
|
|
72274
|
+
encoder.addEventListener("dequeue", () => resolve(), { once: true });
|
|
72275
|
+
}));
|
|
72276
|
+
}
|
|
72277
|
+
}
|
|
72278
|
+
metrics.markFrameRendered();
|
|
72279
|
+
window.__RENDER_METRICS__ = metrics.snapshot();
|
|
72280
|
+
};
|
|
72281
|
+
const runSeekFromFrame = async (startFrame = 0, fallbackReason = captureModeResolution.fallbackReason) => {
|
|
72282
|
+
metrics.setCaptureMode(
|
|
72283
|
+
"seek",
|
|
72284
|
+
fallbackReason
|
|
72285
|
+
);
|
|
72286
|
+
await runSeekCaptureLoop({
|
|
72287
|
+
totalFrames,
|
|
72288
|
+
trimStart,
|
|
72289
|
+
fps,
|
|
72290
|
+
startFrame,
|
|
72291
|
+
seekToFrame,
|
|
72292
|
+
captureFrame,
|
|
72293
|
+
updateProgress: updateVideoProgress,
|
|
72294
|
+
touchHeartbeat: touchRenderHeartbeat
|
|
72295
|
+
});
|
|
72296
|
+
};
|
|
72297
|
+
const effectiveCaptureMode = captureModeResolution.effectiveMode;
|
|
72298
|
+
if (effectiveCaptureMode === "rvfc") {
|
|
72299
|
+
metrics.setCaptureMode("rvfc", null);
|
|
72300
|
+
try {
|
|
72301
|
+
await runPlaybackCaptureLoop({
|
|
72302
|
+
video,
|
|
72303
|
+
totalFrames,
|
|
72304
|
+
trimStart,
|
|
72305
|
+
trimEnd,
|
|
72306
|
+
fps,
|
|
72307
|
+
playbackRate: normalizeCapturePlaybackRate(capturePlaybackRate),
|
|
72308
|
+
maxFrameGap: normalizeMaxFrameGap(maxFrameGap),
|
|
72309
|
+
frameTimeoutMs: seekTimeoutMs,
|
|
72310
|
+
seekToFrame,
|
|
72311
|
+
captureFrame,
|
|
72312
|
+
updateProgress: updateVideoProgress,
|
|
72313
|
+
touchHeartbeat: touchRenderHeartbeat,
|
|
72314
|
+
markPlaybackWait: (durationMs) => {
|
|
72315
|
+
metrics.markDuration("playbackWait", durationMs);
|
|
71605
72316
|
}
|
|
71606
|
-
|
|
71607
|
-
|
|
71608
|
-
|
|
71609
|
-
|
|
71610
|
-
|
|
71611
|
-
|
|
71612
|
-
|
|
72317
|
+
});
|
|
72318
|
+
} catch (error) {
|
|
72319
|
+
if (!(error instanceof PlaybackCaptureFallbackError)) {
|
|
72320
|
+
throw error;
|
|
72321
|
+
}
|
|
72322
|
+
metrics.markPlaybackFallback(error.message);
|
|
72323
|
+
console.warn("[RenderVideo] playback capture fell back to seek", {
|
|
72324
|
+
startFrame: error.startFrame,
|
|
72325
|
+
reason: error.message
|
|
72326
|
+
});
|
|
72327
|
+
touchRenderHeartbeat("fallback-seek");
|
|
72328
|
+
await runSeekFromFrame(error.startFrame, error.message);
|
|
72329
|
+
}
|
|
72330
|
+
} else {
|
|
72331
|
+
await runSeekFromFrame(0);
|
|
72332
|
+
}
|
|
72333
|
+
window.__RENDER_METRICS__ = metrics.snapshot();
|
|
72334
|
+
if (encoderMode === "ffmpeg") {
|
|
72335
|
+
const backpressureHeartbeat = window.setInterval(() => {
|
|
72336
|
+
touchRenderHeartbeat("ffmpeg-backpressure");
|
|
72337
|
+
}, 5e3);
|
|
72338
|
+
try {
|
|
72339
|
+
await measureAsyncMetric(metrics, "framePipeBackpressure", () => flushFfmpegFrameAcks());
|
|
72340
|
+
} finally {
|
|
72341
|
+
window.clearInterval(backpressureHeartbeat);
|
|
72342
|
+
}
|
|
72343
|
+
ffmpegFrameSocket == null ? void 0 : ffmpegFrameSocket.close();
|
|
72344
|
+
window.__RENDER_PROGRESS__ = 99;
|
|
72345
|
+
window.__RENDER_STATUS__ = "done";
|
|
72346
|
+
window.__RENDER_METRICS__ = metrics.snapshot();
|
|
72347
|
+
touchRenderHeartbeat("waiting-ffmpeg-finalize");
|
|
72348
|
+
} else {
|
|
72349
|
+
if (encoder === null || muxer === null || chunkWriteQueue === null) {
|
|
72350
|
+
throw new Error("WebCodecs encoder resources were not initialized");
|
|
72351
|
+
}
|
|
72352
|
+
touchRenderHeartbeat("flushing-video");
|
|
72353
|
+
const videoFlushHeartbeat = setInterval(() => {
|
|
72354
|
+
touchRenderHeartbeat("flushing-video");
|
|
72355
|
+
}, 5e3);
|
|
72356
|
+
try {
|
|
72357
|
+
await measureAsyncMetric(metrics, "videoFlush", () => encoder.flush());
|
|
72358
|
+
} finally {
|
|
72359
|
+
clearInterval(videoFlushHeartbeat);
|
|
72360
|
+
}
|
|
72361
|
+
encoder.close();
|
|
72362
|
+
window.__RENDER_METRICS__ = metrics.snapshot();
|
|
72363
|
+
if (hasAudio && audioBuffer !== null) {
|
|
72364
|
+
window.__RENDER_PROGRESS__ = 91;
|
|
72365
|
+
touchRenderHeartbeat("encoding-audio");
|
|
72366
|
+
const trimmed = trimAudioBuffer(audioBuffer, trimStart, trimEnd);
|
|
72367
|
+
const audioEncoderConfig = {
|
|
72368
|
+
codec: "mp4a.40.2",
|
|
72369
|
+
numberOfChannels: trimmed.numberOfChannels,
|
|
72370
|
+
sampleRate: trimmed.sampleRate,
|
|
72371
|
+
bitrate: audioBitrate
|
|
72372
|
+
};
|
|
72373
|
+
const audioSupport = await AudioEncoder.isConfigSupported(audioEncoderConfig);
|
|
72374
|
+
console.log("[RenderAudio] AAC encoder supported:", audioSupport.supported);
|
|
72375
|
+
if (audioSupport.supported) {
|
|
72376
|
+
const activeMuxer = muxer;
|
|
72377
|
+
const audioEncoder = new AudioEncoder({
|
|
72378
|
+
output: (chunk, metadata) => {
|
|
72379
|
+
activeMuxer.addAudioChunk(chunk, metadata);
|
|
72380
|
+
},
|
|
72381
|
+
error: (err) => {
|
|
72382
|
+
throw new Error(`AudioEncoder error: ${err.message}`);
|
|
72383
|
+
}
|
|
71613
72384
|
});
|
|
71614
|
-
audioEncoder.
|
|
71615
|
-
|
|
71616
|
-
|
|
71617
|
-
|
|
71618
|
-
|
|
72385
|
+
audioEncoder.configure(audioEncoderConfig);
|
|
72386
|
+
const chunkSize = 1024;
|
|
72387
|
+
const firstChannel = trimmed.channels[0];
|
|
72388
|
+
if (firstChannel === void 0) {
|
|
72389
|
+
throw new Error("Decoded audio has no channel data");
|
|
72390
|
+
}
|
|
72391
|
+
const totalSamples = firstChannel.length;
|
|
72392
|
+
for (let offset = 0; offset < totalSamples; offset += chunkSize) {
|
|
72393
|
+
const frameSamples = Math.min(chunkSize, totalSamples - offset);
|
|
72394
|
+
const planarData = new Float32Array(trimmed.numberOfChannels * frameSamples);
|
|
72395
|
+
for (let ch = 0; ch < trimmed.numberOfChannels; ch++) {
|
|
72396
|
+
const channel = trimmed.channels[ch];
|
|
72397
|
+
if (channel === void 0) {
|
|
72398
|
+
throw new Error(`Decoded audio channel ${ch} is unavailable`);
|
|
72399
|
+
}
|
|
72400
|
+
planarData.set(
|
|
72401
|
+
channel.subarray(offset, offset + frameSamples),
|
|
72402
|
+
ch * frameSamples
|
|
72403
|
+
);
|
|
72404
|
+
}
|
|
72405
|
+
const audioData = new AudioData({
|
|
72406
|
+
format: "f32-planar",
|
|
72407
|
+
sampleRate: trimmed.sampleRate,
|
|
72408
|
+
numberOfFrames: frameSamples,
|
|
72409
|
+
numberOfChannels: trimmed.numberOfChannels,
|
|
72410
|
+
timestamp: Math.floor(offset / trimmed.sampleRate * 1e6),
|
|
72411
|
+
data: planarData
|
|
72412
|
+
});
|
|
72413
|
+
measureSyncMetric(metrics, "audioEncode", () => {
|
|
72414
|
+
audioEncoder.encode(audioData);
|
|
71619
72415
|
});
|
|
72416
|
+
audioData.close();
|
|
72417
|
+
if (audioEncoder.encodeQueueSize > 10) {
|
|
72418
|
+
await measureAsyncMetric(metrics, "audioEncode", () => new Promise((resolve) => {
|
|
72419
|
+
audioEncoder.addEventListener("dequeue", () => resolve(), { once: true });
|
|
72420
|
+
}));
|
|
72421
|
+
}
|
|
72422
|
+
touchRenderHeartbeat("encoding-audio");
|
|
72423
|
+
window.__RENDER_METRICS__ = metrics.snapshot();
|
|
72424
|
+
}
|
|
72425
|
+
touchRenderHeartbeat("flushing-audio");
|
|
72426
|
+
const audioFlushHeartbeat = setInterval(() => {
|
|
72427
|
+
touchRenderHeartbeat("flushing-audio");
|
|
72428
|
+
}, 5e3);
|
|
72429
|
+
try {
|
|
72430
|
+
await measureAsyncMetric(metrics, "audioFlush", () => audioEncoder.flush());
|
|
72431
|
+
} finally {
|
|
72432
|
+
clearInterval(audioFlushHeartbeat);
|
|
71620
72433
|
}
|
|
71621
|
-
|
|
72434
|
+
audioEncoder.close();
|
|
72435
|
+
console.log("[RenderAudio] Audio encoding complete");
|
|
72436
|
+
window.__RENDER_AUDIO_STATUS__ = "encoded";
|
|
72437
|
+
} else {
|
|
72438
|
+
console.warn("[RenderAudio] AAC not supported, exporting without audio");
|
|
72439
|
+
window.__RENDER_AUDIO_STATUS__ = "skipped:aac-unsupported";
|
|
71622
72440
|
}
|
|
71623
|
-
|
|
71624
|
-
await audioEncoder.flush();
|
|
71625
|
-
audioEncoder.close();
|
|
71626
|
-
console.log("[RenderAudio] Audio encoding complete");
|
|
71627
|
-
window.__RENDER_AUDIO_STATUS__ = "encoded";
|
|
71628
|
-
} else {
|
|
71629
|
-
console.warn("[RenderAudio] AAC not supported, exporting without audio");
|
|
71630
|
-
window.__RENDER_AUDIO_STATUS__ = "skipped:aac-unsupported";
|
|
72441
|
+
window.__RENDER_PROGRESS__ = 99;
|
|
71631
72442
|
}
|
|
71632
|
-
|
|
72443
|
+
touchRenderHeartbeat("finalizing-muxer");
|
|
72444
|
+
measureSyncMetric(metrics, "muxFinalize", () => {
|
|
72445
|
+
muxer.finalize();
|
|
72446
|
+
});
|
|
72447
|
+
await measureAsyncMetric(metrics, "muxFinalize", () => chunkWriteQueue.flush());
|
|
72448
|
+
window.__RENDER_PROGRESS__ = 100;
|
|
72449
|
+
window.__RENDER_STATUS__ = "done";
|
|
72450
|
+
window.__RENDER_METRICS__ = metrics.snapshot();
|
|
72451
|
+
touchRenderHeartbeat("done");
|
|
71633
72452
|
}
|
|
71634
|
-
touchRenderHeartbeat("finalizing-muxer");
|
|
71635
|
-
muxer.finalize();
|
|
71636
|
-
await chunkWriteQueue.flush();
|
|
71637
|
-
window.__RENDER_PROGRESS__ = 100;
|
|
71638
|
-
window.__RENDER_STATUS__ = "done";
|
|
71639
|
-
touchRenderHeartbeat("done");
|
|
71640
72453
|
app.destroy(true);
|
|
71641
72454
|
} catch (err) {
|
|
71642
72455
|
const message = err instanceof Error ? err.message : String(err);
|