[diffusion] fix: fix realtime webui recording timeline (#27249)
This commit is contained in:
@@ -232,8 +232,7 @@ let recordingEncoder = null;
|
|||||||
let recordingEncoderReady = null;
|
let recordingEncoderReady = null;
|
||||||
let recordingEncoderConfig = null;
|
let recordingEncoderConfig = null;
|
||||||
let recordingFrameIndex = 0;
|
let recordingFrameIndex = 0;
|
||||||
let recordingStartedAt = 0;
|
let recordingFps = DEFAULT_TARGET_FPS;
|
||||||
let recordingFirstFrameAt = 0;
|
|
||||||
let recordingTimer = 0;
|
let recordingTimer = 0;
|
||||||
let recordingSaving = false;
|
let recordingSaving = false;
|
||||||
let recordingEncodeChain = Promise.resolve();
|
let recordingEncodeChain = Promise.resolve();
|
||||||
@@ -246,6 +245,8 @@ const canvas = $("viewport");
|
|||||||
const ctx = canvas.getContext("2d", { alpha: false });
|
const ctx = canvas.getContext("2d", { alpha: false });
|
||||||
const scratchCanvas = document.createElement("canvas");
|
const scratchCanvas = document.createElement("canvas");
|
||||||
const scratchCtx = scratchCanvas.getContext("2d", { alpha: false });
|
const scratchCtx = scratchCanvas.getContext("2d", { alpha: false });
|
||||||
|
const recordingCanvas = document.createElement("canvas");
|
||||||
|
const recordingCtx = recordingCanvas.getContext("2d", { alpha: false });
|
||||||
const playbackController = new RealtimePlaybackController({
|
const playbackController = new RealtimePlaybackController({
|
||||||
targetFps: DEFAULT_TARGET_FPS,
|
targetFps: DEFAULT_TARGET_FPS,
|
||||||
});
|
});
|
||||||
@@ -504,7 +505,7 @@ function updateRecordButton() {
|
|||||||
$("recordLabel").textContent = recordingSaving
|
$("recordLabel").textContent = recordingSaving
|
||||||
? "Saving"
|
? "Saving"
|
||||||
: recordingActive ? "Stop" : "Record";
|
: recordingActive ? "Stop" : "Record";
|
||||||
const elapsedMs = recordingActive ? performance.now() - recordingStartedAt : 0;
|
const elapsedMs = recordingActive ? recordingFrameIndex / Math.max(1, recordingFps) * 1000 : 0;
|
||||||
$("recordDuration").textContent = formatRecordingDuration(elapsedMs);
|
$("recordDuration").textContent = formatRecordingDuration(elapsedMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,8 +529,7 @@ function startRecording() {
|
|||||||
recordingEncoderReady = null;
|
recordingEncoderReady = null;
|
||||||
recordingEncoderConfig = null;
|
recordingEncoderConfig = null;
|
||||||
recordingFrameIndex = 0;
|
recordingFrameIndex = 0;
|
||||||
recordingStartedAt = performance.now();
|
recordingFps = Math.max(1, previewPlaybackTargetFps());
|
||||||
recordingFirstFrameAt = 0;
|
|
||||||
recordingEncodeChain = Promise.resolve();
|
recordingEncodeChain = Promise.resolve();
|
||||||
recordingTimer = window.setInterval(updateRecordButton, 250);
|
recordingTimer = window.setInterval(updateRecordButton, 250);
|
||||||
updateRecordButton();
|
updateRecordButton();
|
||||||
@@ -587,25 +587,34 @@ async function stopRecording() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function captureRecordingFrame(item, now) {
|
function recordDecodedFrameBatch(decodedFrames) {
|
||||||
if (!recordingActive) return;
|
if (!recordingActive || recordingSaving) return;
|
||||||
if (!recordingFirstFrameAt) recordingFirstFrameAt = now;
|
for (const item of decodedFrames) {
|
||||||
const timestamp = Math.max(0, Math.round((now - recordingFirstFrameAt) * 1000));
|
if (!recordingActive) break;
|
||||||
const duration = Math.round(1_000_000 / Math.max(1, previewPlaybackTargetFps()));
|
recordDecodedFrame(item.image);
|
||||||
|
}
|
||||||
|
updateRecordButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordDecodedFrame(image) {
|
||||||
|
if (!recordingActive || recordingSaving) return;
|
||||||
|
const frameIndex = recordingFrameIndex;
|
||||||
|
const duration = Math.round(1_000_000 / Math.max(1, recordingFps));
|
||||||
|
const timestamp = frameIndex * duration;
|
||||||
let frame;
|
let frame;
|
||||||
try {
|
try {
|
||||||
frame = new VideoFrame(canvas, { timestamp, duration });
|
frame = createRecordingFrame(image, timestamp, duration);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
recordingActive = false;
|
recordingActive = false;
|
||||||
addHistory(error.message || "recording frame capture failed");
|
addHistory(error.message || "recording frame capture failed");
|
||||||
updateRecordButton();
|
updateRecordButton();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const index = recordingFrameIndex++;
|
recordingFrameIndex += 1;
|
||||||
recordingEncodeChain = recordingEncodeChain
|
recordingEncodeChain = recordingEncodeChain
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await ensureRecordingEncoder(frame.displayWidth, frame.displayHeight);
|
await ensureRecordingEncoder(frame.displayWidth, frame.displayHeight);
|
||||||
recordingEncoder.encode(frame, { keyFrame: index === 0 || index % 120 === 0 });
|
recordingEncoder.encode(frame, { keyFrame: frameIndex === 0 || frameIndex % 120 === 0 });
|
||||||
frame.close();
|
frame.close();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -616,6 +625,18 @@ function captureRecordingFrame(item, now) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createRecordingFrame(image, timestamp, duration) {
|
||||||
|
if (image instanceof ImageData) {
|
||||||
|
if (recordingCanvas.width !== image.width || recordingCanvas.height !== image.height) {
|
||||||
|
recordingCanvas.width = image.width;
|
||||||
|
recordingCanvas.height = image.height;
|
||||||
|
}
|
||||||
|
recordingCtx.putImageData(image, 0, 0);
|
||||||
|
return new VideoFrame(recordingCanvas, { timestamp, duration });
|
||||||
|
}
|
||||||
|
return new VideoFrame(image, { timestamp, duration });
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureRecordingEncoder(width, height) {
|
async function ensureRecordingEncoder(width, height) {
|
||||||
if (recordingEncoderReady) return recordingEncoderReady;
|
if (recordingEncoderReady) return recordingEncoderReady;
|
||||||
recordingEncoderReady = createRecordingEncoder(width, height);
|
recordingEncoderReady = createRecordingEncoder(width, height);
|
||||||
@@ -623,7 +644,7 @@ async function ensureRecordingEncoder(width, height) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createRecordingEncoder(width, height) {
|
async function createRecordingEncoder(width, height) {
|
||||||
const fps = Math.max(1, previewPlaybackTargetFps());
|
const fps = Math.max(1, recordingFps);
|
||||||
const bitrate = Math.round(Math.min(
|
const bitrate = Math.round(Math.min(
|
||||||
180_000_000,
|
180_000_000,
|
||||||
Math.max(24_000_000, width * height * fps * 0.8),
|
Math.max(24_000_000, width * height * fps * 0.8),
|
||||||
@@ -710,7 +731,7 @@ function buildRecordingMp4() {
|
|||||||
function normalizeRecordingSamples(samples) {
|
function normalizeRecordingSamples(samples) {
|
||||||
const ordered = [...samples].sort((left, right) => left.timestamp - right.timestamp);
|
const ordered = [...samples].sort((left, right) => left.timestamp - right.timestamp);
|
||||||
const timescale = 90_000;
|
const timescale = 90_000;
|
||||||
const fallbackDuration = Math.round(timescale / Math.max(1, previewPlaybackTargetFps()));
|
const fallbackDuration = Math.round(timescale / Math.max(1, recordingFps));
|
||||||
const normalized = ordered.map((sample) => ({
|
const normalized = ordered.map((sample) => ({
|
||||||
...sample,
|
...sample,
|
||||||
time: Math.round(sample.timestamp * timescale / 1_000_000),
|
time: Math.round(sample.timestamp * timescale / 1_000_000),
|
||||||
@@ -961,6 +982,7 @@ function enqueueDecodeBatch(header, data, epoch) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function trimDecodeQueue() {
|
function trimDecodeQueue() {
|
||||||
|
if (recordingActive) return;
|
||||||
if (!decodeQueue.length) return;
|
if (!decodeQueue.length) return;
|
||||||
const playback = playbackController.snapshot();
|
const playback = playbackController.snapshot();
|
||||||
const decodeWindowSeconds = renderedPreviewFrames
|
const decodeWindowSeconds = renderedPreviewFrames
|
||||||
@@ -1249,7 +1271,6 @@ function renderLoop(now) {
|
|||||||
if (decision.action === "draw") {
|
if (decision.action === "draw") {
|
||||||
const item = decision.frame;
|
const item = decision.frame;
|
||||||
drawFrame(item.image);
|
drawFrame(item.image);
|
||||||
captureRecordingFrame(item, now);
|
|
||||||
fpsSamples.push(now);
|
fpsSamples.push(now);
|
||||||
fpsSamples = fpsSamples.filter((t) => now - t < 1000);
|
fpsSamples = fpsSamples.filter((t) => now - t < 1000);
|
||||||
const renderedFps = String(fpsSamples.length);
|
const renderedFps = String(fpsSamples.length);
|
||||||
@@ -1536,6 +1557,8 @@ async function decodeAndEnqueueFrameBatch(header, data, epoch) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
|
// record source frames before preview playback can hold or drop for latency
|
||||||
|
recordDecodedFrameBatch(decodedFrames);
|
||||||
const enqueueResult = playbackController.enqueueDecodedFrames(header, decodedFrames, now);
|
const enqueueResult = playbackController.enqueueDecodedFrames(header, decodedFrames, now);
|
||||||
closeFrames(enqueueResult.droppedFrames);
|
closeFrames(enqueueResult.droppedFrames);
|
||||||
if (enqueueResult.cutover?.latencyMs) {
|
if (enqueueResult.cutover?.latencyMs) {
|
||||||
|
|||||||
@@ -163,6 +163,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<script src="./playback_controller.js?v=realtime-playback-v13"></script>
|
<script src="./playback_controller.js?v=realtime-playback-v13"></script>
|
||||||
<script src="./app.js?v=realtime-record-v72"></script>
|
<script src="./app.js?v=realtime-record-v73"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user