mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-04 21:17:30 +00:00
168 lines
6.0 KiB
TypeScript
168 lines
6.0 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// Maps studio-triggered renders into the existing `render_complete` /
|
|
// `render_error` telemetry events with `source: "studio"`, so they land
|
|
// alongside CLI renders in one unified taxonomy.
|
|
//
|
|
// Kept in its own file so `studioServer.ts` only needs two function calls.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import { freemem } from "node:os";
|
|
import type { CanvasResolution, Fps } from "@hyperframes/core";
|
|
import { fpsToNumber } from "@hyperframes/core";
|
|
import type { RenderJob, RenderPerfSummary } from "@hyperframes/producer";
|
|
import { trackRenderComplete, trackRenderError } from "../telemetry/events.js";
|
|
import {
|
|
renderJobObservabilityTelemetryPayload,
|
|
renderObservabilityTelemetryPayload,
|
|
} from "../telemetry/renderObservability.js";
|
|
import { bytesToMb } from "../telemetry/system.js";
|
|
import type { RenderCpuTelemetryPayload } from "../telemetry/renderCpu.js";
|
|
|
|
export interface StudioRenderOpts {
|
|
fps: Fps;
|
|
quality: string;
|
|
format?: "mp4" | "webm" | "mov" | "gif";
|
|
/** Named canvas preset the Studio request asked for; undefined renders at the composition's native dimensions. */
|
|
outputResolution?: CanvasResolution;
|
|
useGpu?: boolean;
|
|
// Telemetry id of the browser user who triggered the render, so the render
|
|
// outcome joins their studio_session_start / studio_render_start events.
|
|
// Undefined for older studio clients → falls back to the install anonymousId.
|
|
distinctId?: string;
|
|
/**
|
|
* The browser profile that triggered this render has telemetry disabled.
|
|
*
|
|
* The CLI's own policy cannot see a localStorage opt-out or `DoNotTrack` in
|
|
* someone else's browser, so without this the server happily emitted
|
|
* render_complete / render_error for a user who had opted out — the events
|
|
* merely landed on the install id instead of theirs, which is worse, not
|
|
* better. Explicit `true` only: an old client sends nothing here, and that
|
|
* is not consent withdrawn.
|
|
*/
|
|
telemetryOptOut?: boolean;
|
|
}
|
|
|
|
type RenderCompleteProps = Parameters<typeof trackRenderComplete>[0];
|
|
|
|
function memSnapshot(): { peakMemoryMb: number; memoryFreeMb: number } {
|
|
return {
|
|
peakMemoryMb: bytesToMb(process.memoryUsage.rss()),
|
|
memoryFreeMb: bytesToMb(freemem()),
|
|
};
|
|
}
|
|
|
|
function stagesPayload(stages: Record<string, number>): Partial<RenderCompleteProps> {
|
|
return {
|
|
stageCompileMs: stages.compileMs,
|
|
stageVideoExtractMs: stages.videoExtractMs,
|
|
stageAudioProcessMs: stages.audioProcessMs,
|
|
stageCaptureMs: stages.captureMs,
|
|
stageCaptureSetupMs: stages.captureSetupMs,
|
|
stageCaptureFrameMs: stages.captureFrameMs,
|
|
stageEncodeMs: stages.encodeMs,
|
|
stageAssembleMs: stages.assembleMs,
|
|
};
|
|
}
|
|
|
|
function extractPayload(
|
|
extract: RenderPerfSummary["videoExtractBreakdown"],
|
|
): Partial<RenderCompleteProps> {
|
|
if (!extract) return {};
|
|
return {
|
|
extractResolveMs: extract.resolveMs,
|
|
extractHdrProbeMs: extract.hdrProbeMs,
|
|
extractHdrPreflightMs: extract.hdrPreflightMs,
|
|
extractHdrPreflightCount: extract.hdrPreflightCount,
|
|
extractVfrProbeMs: extract.vfrProbeMs,
|
|
extractVfrPreflightMs: extract.vfrPreflightMs,
|
|
extractVfrPreflightCount: extract.vfrPreflightCount,
|
|
extractPhase3Ms: extract.extractMs,
|
|
extractCacheHits: extract.cacheHits,
|
|
extractCacheMisses: extract.cacheMisses,
|
|
};
|
|
}
|
|
|
|
function perfPayload(
|
|
perf: RenderPerfSummary | undefined,
|
|
elapsedMs: number,
|
|
): Partial<RenderCompleteProps> {
|
|
if (!perf) return {};
|
|
const compositionDurationMs = Math.round(perf.compositionDurationSeconds * 1000);
|
|
const speedRatio =
|
|
compositionDurationMs > 0 && elapsedMs > 0
|
|
? Math.round((compositionDurationMs / elapsedMs) * 100) / 100
|
|
: undefined;
|
|
return {
|
|
workers: perf.workers,
|
|
compositionDurationMs,
|
|
compositionWidth: perf.resolution.width,
|
|
compositionHeight: perf.resolution.height,
|
|
totalFrames: perf.totalFrames,
|
|
speedRatio,
|
|
captureAvgMs: perf.captureAvgMs,
|
|
captureSeekAvgMs: perf.captureSeekAvgMs,
|
|
captureBeforeCaptureAvgMs: perf.captureBeforeCaptureAvgMs,
|
|
captureScreenshotAvgMs: perf.captureScreenshotAvgMs,
|
|
encoderWaitAvgMs: perf.encoderWaitAvgMs,
|
|
capturePeakMs: perf.capturePeakMs,
|
|
tmpPeakBytes: perf.tmpPeakBytes,
|
|
...stagesPayload(perf.stages),
|
|
...extractPayload(perf.videoExtractBreakdown),
|
|
...renderObservabilityTelemetryPayload(perf.observability),
|
|
};
|
|
}
|
|
|
|
export function emitStudioRenderError(
|
|
opts: StudioRenderOpts,
|
|
elapsedMs: number,
|
|
failedStage: string | undefined,
|
|
err: unknown,
|
|
job: RenderJob | undefined,
|
|
cpuUsage: RenderCpuTelemetryPayload = {},
|
|
): void {
|
|
// `workers` is intentionally omitted: studio renders don't accept a
|
|
// user-supplied worker count (the producer picks its default), so on early
|
|
// failures we genuinely don't know one. The CLI side has the value from
|
|
// `options.workers` even before `job.perfSummary` exists; studio doesn't.
|
|
if (opts.telemetryOptOut === true) return;
|
|
trackRenderError({
|
|
fps: fpsToNumber(opts.fps),
|
|
quality: opts.quality,
|
|
docker: false,
|
|
gpu: opts.useGpu === true,
|
|
source: "studio",
|
|
failedStage,
|
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
elapsedMs,
|
|
outputFormat: opts.format,
|
|
outputResolutionPreset: opts.outputResolution,
|
|
distinctId: opts.distinctId,
|
|
...renderJobObservabilityTelemetryPayload(job),
|
|
...memSnapshot(),
|
|
...cpuUsage,
|
|
});
|
|
}
|
|
|
|
export function emitStudioRenderComplete(
|
|
opts: StudioRenderOpts,
|
|
elapsedMs: number,
|
|
perf: RenderPerfSummary | undefined,
|
|
cpuUsage: RenderCpuTelemetryPayload = {},
|
|
): void {
|
|
if (opts.telemetryOptOut === true) return;
|
|
trackRenderComplete({
|
|
durationMs: elapsedMs,
|
|
fps: fpsToNumber(opts.fps),
|
|
quality: opts.quality,
|
|
docker: false,
|
|
gpu: opts.useGpu === true,
|
|
source: "studio",
|
|
outputFormat: opts.format,
|
|
outputResolutionPreset: opts.outputResolution,
|
|
distinctId: opts.distinctId,
|
|
...perfPayload(perf, elapsedMs),
|
|
...memSnapshot(),
|
|
...cpuUsage,
|
|
});
|
|
}
|