/**
 * Video Frame Extractor Service
 *
 * Pre-extracts video frames using FFmpeg for frame-accurate rendering.
 * Videos are replaced with <img> elements during capture.
 */
import { type FpsInput } from "@hyperframes/core";
import { type VideoMetadata } from "../utils/ffprobe.js";
import { type HdrTransfer } from "../utils/hdr.js";
import { type EngineConfig } from "../config.js";
import { type CacheFrameFormat } from "./extractionCache.js";
export interface VideoElement {
    id: string;
    src: string;
    start: number;
    end: number;
    mediaStart: number;
    loop: boolean;
    hasAudio: boolean;
}
export interface ExtractedFrames {
    videoId: string;
    srcPath: string;
    outputDir: string;
    framePattern: string;
    fps: number;
    totalFrames: number;
    metadata: VideoMetadata;
    framePaths: Map<number, string>;
    /**
     * True when the extractor owns `outputDir` and cleanup should rm it when
     * the render ends. Cache hits set this to false so the shared entry isn't
     * deleted by a single render's cleanup — the cache dir is owned by the
     * caller's gc policy, not any one render.
     */
    ownedByLookup?: boolean;
}
/**
 * The single source of truth for the source-video frame-extraction allow-list.
 * The CLI flag parser, the producer HTTP server, and the distributed-config
 * validator all validate against this same set via {@link isVideoFrameFormat}
 * so the boundaries can't drift when a new format is added.
 */
export declare const VIDEO_FRAME_FORMATS: readonly ["auto", "jpg", "png"];
export type VideoFrameFormat = (typeof VIDEO_FRAME_FORMATS)[number];
/** Runtime guard for {@link VideoFrameFormat} over an untrusted value. */
export declare function isVideoFrameFormat(value: unknown): value is VideoFrameFormat;
/**
 * Resolve the frame count produced for a requested extraction duration.
 *
 * CFR extraction uses FFmpeg's fps filter, whose end boundary rounds to the
 * nearest frame. The VFR path normalizes with `-fps_mode cfr -r`, whose end
 * boundary rounds up. Keep this calculation shared by superset slicing and
 * producer coverage accounting so a complete VFR extraction cannot be
 * rejected because the two paths disagree by one frame.
 */
export declare function extractionFrameCountForDuration(durationSeconds: number, fps: FpsInput, isVFR: boolean): number;
export interface ExtractionOptions {
    /** Exact configured rate. Rational rates are passed to FFmpeg verbatim. */
    fps: FpsInput;
    outputDir: string;
    quality?: number;
    format?: VideoFrameFormat;
    sdrToHdrTransfer?: HdrTransfer;
    /** Extract exactly one frame at `startTime`. Used only after ffprobe has
     *  resolved the actual final decoded-frame timestamp for a held tail. */
    finalFrameOnly?: boolean;
    /**
     * Absolute composition/timeline end in seconds. Applied only after source
     * metadata resolves open-ended/natural-duration media. Invisible negative
     * preroll is trimmed while advancing mediaStart to preserve source alignment.
     */
    timelineEnd?: number;
    /**
     * Bounded per-source FFmpeg retries. Default 0 preserves stable behavior;
     * the producer may canary at most one retry after observing typed failures.
     */
    maxTransientRetries?: number;
    /**
     * Collect metadata-probe failures into `ExtractionResult.errors` instead
     * of preserving the legacy Promise rejection. Default false; only the
     * candidate enforce lane may opt into typed aggregation.
     */
    collectProbeFailures?: boolean;
}
/**
 * Per-phase timings and counters emitted by `extractAllVideoFrames`.
 *
 * Used by the producer to surface `perfSummary.videoExtractBreakdown` — without
 * this breakdown, a single `videoExtractMs` stage timing hides where cost lives
 * (HDR preflight, VFR classification, per-video ffmpeg extract) when tuning renders.
 *
 * Field semantics:
 *   - *Ms fields are wall-clock durations inside each phase.
 *   - *Count fields report how many sources triggered that phase.
 *   - extractMs wraps the parallel `extractVideoFramesRange` calls; it
 *     reflects max-across-parallel-workers, not sum.
 *   - hdrPreflightMs includes its probe-time sibling (hdrProbeMs); the
 *     probe-only field is a finer decomposition, not a separate carve-out.
 *   - vfrPreflightCount reports sources classified as VFR and routed through
 *     the one-pass `-fps_mode cfr -r` extraction path. DEFINITION CHANGE:
 *     before the one-pass refactor, vfrPreflightMs timed a per-source
 *     VFR-to-CFR re-encode and could reach seconds; it now times only the
 *     (promise-cached) classification probe and is expected to be ~0.
 *     Dashboards alerting on vfrPreflightMs thresholds should key on
 *     vfrPreflightCount or extractMs instead.
 */
export interface ExtractionPhaseBreakdown {
    resolveMs: number;
    /** Publishes that could not land atomically — the render still succeeded
     *  from the partial dir, but future renders re-extract. A rising rate is
     *  the first signal that warm renders are silently going cold. */
    cachePublishFailures: number;
    /** Entries evicted by the post-extraction LRU sweep. */
    cacheGcEvictions: number;
    /** Bytes reclaimed by the LRU sweep. */
    cacheGcBytesFreed: number;
    /** Aged .partial-* dirs (crashed writers) removed by the sweep. */
    cacheAgedPartialsCleared: number;
    hdrProbeMs: number;
    hdrPreflightMs: number;
    hdrPreflightCount: number;
    vfrProbeMs: number;
    vfrPreflightMs: number;
    vfrPreflightCount: number;
    extractMs: number;
    cacheHits: number;
    cacheMisses: number;
    /** Number of per-source transient failures retried inside this extraction. */
    transientRetries?: number;
}
export type VideoExtractionFailureKind = "cancelled" | "source_missing" | "source_rejected" | "download_not_found" | "download_transient" | "invalid_media" | "media_start_out_of_range" | "ffmpeg_unavailable" | "ffmpeg_timeout" | "ffmpeg_transient" | "ffmpeg_failed" | "zero_output" | "internal";
export interface VideoExtractionFailure {
    videoId: string;
    /** Always populated by this engine version; optional for source compatibility with older consumers. */
    kind?: VideoExtractionFailureKind;
    /** Always populated by this engine version; absent legacy values fail closed. */
    retryable?: boolean;
    /**
     * Operator diagnostic retained inside the engine result. Producer-facing
     * errors must summarize `kind`/counts and must not forward this field: it
     * can contain a local path or a signed source URL.
     */
    error: string;
}
export declare class VideoSourceExtractionError extends Error {
    readonly kind: VideoExtractionFailureKind;
    readonly retryable: boolean;
    readonly diagnostic: string;
    readonly hyperframesVideoSourceExtractionError: true;
    constructor(kind: VideoExtractionFailureKind, retryable: boolean, message: string, diagnostic?: string);
}
export declare function isVideoSourceExtractionError(error: unknown): error is VideoSourceExtractionError;
/**
 * Convert legacy/raw downloader and filesystem errors into the bounded
 * extraction taxonomy. New extraction code should throw
 * `VideoSourceExtractionError` directly; this classifier keeps older utility
 * boundaries safe while they migrate.
 */
export declare function classifyVideoExtractionError(error: unknown): VideoSourceExtractionError;
export declare function runVideoExtractionWithRetry<T>(operation: () => Promise<T>, options?: {
    signal?: AbortSignal;
    onRetry?: () => Promise<void> | void;
    maxTransientRetries?: number;
}): Promise<{
    result: T;
    retries: number;
}>;
export interface ExtractionResult {
    success: boolean;
    extracted: ExtractedFrames[];
    errors: VideoExtractionFailure[];
    totalFramesExtracted: number;
    durationMs: number;
    phaseBreakdown: ExtractionPhaseBreakdown;
}
export declare function parseVideoElements(html: string): VideoElement[];
export interface ImageElement {
    id: string;
    src: string;
    start: number;
    end: number;
}
export declare function parseImageElements(html: string): ImageElement[];
export declare function extractVideoFramesRange(videoPath: string, videoId: string, startTime: number, duration: number, options: ExtractionOptions, signal?: AbortSignal, config?: Partial<Pick<EngineConfig, "ffmpegProcessTimeout">>, 
/**
 * Override the output directory for this extraction. When provided, frames
 * are written directly into `outputDirOverride` (no per-videoId subdir).
 * Used by the cache layer to materialize frames straight into the keyed
 * cache entry directory.
 */
outputDirOverride?: string): Promise<ExtractedFrames>;
export declare function classifyFfmpegSpawnError(error: unknown, stderr?: string): VideoSourceExtractionError;
/**
 * Return the range that can actually produce video frames.
 *
 * Container duration may include a longer audio stream or mux padding. Using
 * it for video extraction planning can reserve raw-frame scratch for seconds
 * where no video frames exist. `extractMediaMetadata` already falls back to
 * the container duration when ffprobe omits the stream duration; keep the
 * explicit fallback here for callers supplying older/manual metadata.
 */
export declare function resolvePlayableVideoDuration(metadata: VideoMetadata): number;
export interface TimelineExtractionWindow {
    compositionStart: number;
    mediaStart: number;
    durationSeconds: number;
    /**
     * Preserve the authored timeline origin and mediaStart for lookup. This is
     * required when a looped visible interval crosses a source boundary and
     * still needs modulo phase against the complete extracted source cycle.
     */
    preserveTimelinePhase?: boolean;
    /**
     * Keep the authored end while rebasing start/mediaStart to the extracted
     * source suffix. Non-looping lookup then holds the suffix's final frame
     * through the remainder of the authored slot.
     */
    preserveTimelineEnd?: boolean;
    /** This window reaches a held tail and must be checked against the actual
     *  final decoded-frame timestamp before extraction. */
    ensureFinalFrame?: boolean;
    /** Source timestamp used by FFmpeg when it differs from the logical lookup
     *  mediaStart (the one-frame held-tail representation). */
    extractionMediaStart?: number;
    /** FFmpeg emits one decoded frame; lookup then holds that frame. */
    finalFrameOnly?: boolean;
}
type TimelineWindowVideo = Pick<VideoElement, "start" | "end" | "mediaStart"> & Partial<Pick<VideoElement, "loop">>;
/**
 * Intersect an authored slot with the render timeline, then select the
 * smallest playable source range that preserves timeline lookup semantics.
 *
 * A finite authored slot can outlive the source. In that case FFmpeg should
 * still extract at most one source range: lookup either wraps that range for
 * loops or holds its final frame for non-looping video. Keeping the authored
 * timeline origin separate from the extracted range is what makes both
 * behaviours survive the source-duration cap.
 */
export declare function resolveTimelineExtractionWindow(video: TimelineWindowVideo, resolvedDuration: number, timelineEnd?: number, sourceDuration?: number): TimelineExtractionWindow;
/**
 * Replace a held-tail suffix that starts at/after the final decoded timestamp
 * with one exact frame. This keeps raw HDR scratch O(one frame) without
 * assuming a one-second seek window contains a CFR/VFR timestamp.
 */
export declare function resolveFinalFrameExtractionWindow(videoPath: string, video: TimelineWindowVideo, metadata: VideoMetadata, window: TimelineExtractionWindow, signal?: AbortSignal): Promise<TimelineExtractionWindow>;
/** Resolve source duration first, then intersect it with the render timeline. */
export declare function resolveVideoExtractionWindow(video: TimelineWindowVideo, metadata: VideoMetadata, timelineEnd?: number): TimelineExtractionWindow;
export declare function resolveVideoExtractionDuration(video: TimelineWindowVideo, metadata: VideoMetadata, timelineEnd?: number): number;
export declare function codecMayHaveAlpha(codec: string | undefined): boolean;
export declare function decoderForCodec(codec: string | undefined): string;
export declare function resolveFrameFormat(metadata: VideoMetadata, requested?: VideoFrameFormat): CacheFrameFormat;
/**
 * Resolve a relative `<video src>` to a filesystem path the way the browser
 * resolves it as a URL. Browsers clamp `..` segments at the served origin's
 * root; `path.join(projectDir, "../assets/foo")` does not. So a sub-comp
 * `<video src="../assets/foo">` loads in the page (browser clamps to
 * `<projectDir>/assets/foo`) but the filesystem-side resolver lands at
 * `<parentOfProjectDir>/assets/foo` — file missing, extraction skipped,
 * the rendered output shows the video's first frame for the whole clip.
 *
 * The clamp covers two escape patterns: leading `..` (`../assets/foo`) AND
 * mid-path escapes (`assets/../../foo`) that `path.join` collapses past the
 * project root silently. Both fall back to a project-rooted candidate that
 * strips traversal from the resolved path.
 *
 * Returns the first existing candidate, or the base-dir join on miss so
 * the caller's `existsSync` check produces a stable error path.
 */
export declare function resolveProjectRelativeSrc(src: string, baseDir: string, compiledDir?: string): string;
export declare function extractAllVideoFrames(videos: VideoElement[], baseDir: string, options: ExtractionOptions, signal?: AbortSignal, config?: Partial<Pick<EngineConfig, "ffmpegProcessTimeout" | "extractCacheDir" | "extractCacheMaxBytes">>, compiledDir?: string): Promise<ExtractionResult>;
export declare function getFrameAtTime(extracted: ExtractedFrames, globalTime: number, videoStart: number, loop?: boolean, mediaStart?: number): string | null;
/**
 * Whether a media source is shorter than its `data-duration` slot by more than
 * the compiler tolerance. The calculation stays tag-agnostic; current in-repo
 * warnings call it for audio only because video slots may intentionally outlive
 * their source and hold the final frame.
 */
export declare function analyzeClipMediaFit(params: {
    /** Timeline slot length in seconds — `end - start` (a.k.a. data-duration). */
    slotSeconds: number;
    /** Playable source media after the trim offset — `duration - mediaStart`. */
    mediaSeconds: number;
    /** Looping clips repeat to fill the slot, so they never fall short. */
    loop?: boolean;
}): {
    shortfallSeconds: number;
    toleranceSeconds: number;
} | null;
export declare class FrameLookupTable {
    private videos;
    private orderedVideos;
    private activeVideoIds;
    private startCursor;
    private lastTime;
    addVideo(extracted: ExtractedFrames, start: number, end: number, mediaStart: number, loop?: boolean): void;
    getFrame(videoId: string, globalTime: number): string | null;
    private resetActiveState;
    private refreshActiveSet;
    getActiveFramePayloads(globalTime: number): Map<string, {
        framePath: string;
        frameIndex: number;
    }>;
    getActiveFrames(globalTime: number): Map<string, string>;
    cleanup(): void;
}
export declare function createFrameLookupTable(videos: VideoElement[], extracted: ExtractedFrames[]): FrameLookupTable;
export {};
//# sourceMappingURL=videoFrameExtractor.d.ts.map