import { TWMixin } from "../gui/TWMixin2.js";
import { withSpanSync } from "../otel/tracingHelpers.js";
import { PRIORITY_VIDEO } from "../preview/FrameController.js";
import { updateAnimations } from "./updateAnimations.js";
import { __decorate } from "../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
import { EFMedia } from "./EFMedia.js";
import { DelayedLoadingState } from "../DelayedLoadingState.js";
import { MainVideoInputCache } from "./EFMedia/videoTasks/MainVideoInputCache.js";
import { ScrubInputCache } from "./EFMedia/videoTasks/ScrubInputCache.js";
import debug from "debug";
import { css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { context, trace } from "@opentelemetry/api";
import { createRef, ref } from "lit/directives/ref.js";

//#region src/elements/EFVideo.ts
const mainVideoInputCache = new MainVideoInputCache();
const scrubInputCache = new ScrubInputCache();
const log = debug("ef:elements:EFVideo");
var VideoSeekTask = class {
	constructor() {
		this.value = void 0;
		this.task = void 0;
		this.taskComplete = Promise.resolve(void 0);
	}
	#resolve;
	begin() {
		this.taskComplete = new Promise((resolve) => {
			this.#resolve = resolve;
		});
	}
	complete(sample) {
		this.value = sample;
		this.#resolve?.(sample);
	}
	abort() {
		this.#resolve?.(void 0);
	}
};
let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
	static {
		this.styles = [css`
      :host {
        display: block;
        position: relative;
        object-fit: contain;
        object-position: center;
      }
      canvas {
        overflow: hidden;
        position: static;
        width: 100%;
        height: 100%;
        object-fit: inherit;
        object-position: inherit;
        margin: 0;
        padding: 0;
        border: none;
        outline: none;
        box-shadow: none;
      }
      .loading-overlay {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        height: 2px;
        overflow: hidden;
        z-index: 10;
        pointer-events: none;
        background: var(--ef-color-loading-spinner-track, rgba(255, 255, 255, 0.1));
      }
      .loading-bar {
        position: absolute;
        top: 0;
        height: 100%;
        width: 40%;
        background: var(--ef-color-loading-spinner-fill, rgba(255, 255, 255, 0.8));
        animation: loading-sweep 1.4s ease-in-out infinite;
      }
      @keyframes loading-sweep {
        0% { left: -40%; }
        100% { left: 140%; }
      }
    `];
	}
	/**
	* Cached video sample for the current frame.
	* Set by prepareFrame(), consumed by renderFrame().
	*/
	#cachedVideoSample = void 0;
	#cachedVideoSampleTimeMs = void 0;
	/**
	* Quality upgrade intent tracking.
	* Tracks what upgrade tasks were last submitted to avoid redundant scheduler calls.
	*/
	#upgradeState = null;
	/**
	* Standalone upgrade controller for elements without a timegroup.
	*/
	#standaloneUpgradeController = null;
	/**
	* Set to true while renderToVideo is executing to suppress background
	* quality upgrade tasks that would race with the render pipeline.
	*/
	#renderingToVideo = false;
	/**
	* Stable per-instance identifier for the quality upgrade scheduler.
	* Uses this.id when available; falls back to a generated unique string so
	* elements without an id attribute never collide with each other.
	*/
	#upgradeOwnerId = crypto.randomUUID();
	/**
	* Current rendition being displayed (for observability).
	*/
	#currentRenditionId = void 0;
	/**
	* Get the current rendition being displayed.
	* @public
	*/
	get currentRenditionId() {
		return this.#currentRenditionId;
	}
	/**
	* Query readiness state for a given time.
	* @implements FrameRenderable
	*
	* Note: The timeMs parameter is the root timegroup's time. We check against
	* this.currentSourceTimeMs since that's what we cache in prepareFrame.
	*/
	getFrameState(_timeMs) {
		const sourceTimeMs = this.currentSourceTimeMs;
		const hasCache = this.#cachedVideoSample !== void 0 && this.#cachedVideoSampleTimeMs === sourceTimeMs;
		return {
			needsPreparation: !hasCache,
			isReady: hasCache,
			priority: PRIORITY_VIDEO
		};
	}
	/**
	* Async preparation - seeks video and caches the sample.
	* @implements FrameRenderable
	*
	* Note: The timeMs parameter is the root timegroup's time. We ignore it and
	* use this.currentSourceTimeMs instead, which accounts for:
	* - Our position within the parent timegroup (ownCurrentTimeMs)
	* - Source trimming (sourceIn/sourceOut or trimStart/trimEnd)
	*/
	async prepareFrame(_timeMs, signal) {
		signal.throwIfAborted();
		this.unifiedVideoSeekTask.begin();
		this.#delayedLoadingState.startLoading("prepare-frame", "");
		try {
			const sourceTimeMs = this.currentSourceTimeMs;
			const mediaEngine = await this.getMediaEngine(signal);
			if (!mediaEngine) {
				this.#cachedVideoSample = void 0;
				this.#cachedVideoSampleTimeMs = sourceTimeMs;
				this.unifiedVideoSeekTask.complete(void 0);
				return;
			}
			signal.throwIfAborted();
			try {
				const videoSample = await this.#fetchVideoSampleForFrame(mediaEngine, sourceTimeMs, signal);
				signal.throwIfAborted();
				this.#cachedVideoSample = videoSample;
				this.#cachedVideoSampleTimeMs = sourceTimeMs;
				this.unifiedVideoSeekTask.complete(videoSample);
			} catch (error) {
				if (error instanceof DOMException && error.name === "AbortError") {
					this.unifiedVideoSeekTask.abort();
					throw error;
				}
				console.warn(`Video seek error at ${sourceTimeMs}ms:`, error);
				this.#cachedVideoSample = void 0;
				this.#cachedVideoSampleTimeMs = sourceTimeMs;
				this.unifiedVideoSeekTask.complete(void 0);
			}
		} finally {
			this.#delayedLoadingState.clearLoading("prepare-frame");
		}
	}
	/**
	* Synchronous render - paints cached video sample to canvas.
	* @implements FrameRenderable
	*
	* Note: The timeMs parameter is the root timegroup's time. We use
	* this.currentSourceTimeMs to match what prepareFrame cached.
	*/
	renderFrame(_timeMs) {
		const sourceTimeMs = this.currentSourceTimeMs;
		if (this.#cachedVideoSampleTimeMs === sourceTimeMs && this.#cachedVideoSample) {
			const videoFrame = this.#cachedVideoSample.toVideoFrame();
			try {
				this.displayFrame(videoFrame, sourceTimeMs);
			} finally {
				videoFrame.close();
			}
		}
		if (!this.parentTimegroup) updateAnimations(this);
	}
	/**
	* Fetch video sample for a given time.
	*
	* Uses a quality routing strategy:
	* - In production rendering: always use main (full quality) track
	* - In preview mode: try scrub track first for faster scrubbing, fall back to main
	* - If main track segment is already cached: use it (avoid redundant lower-quality fetch)
	*/
	async #fetchVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal) {
		const mainTrack = mediaEngine.tracks.video;
		if (mainTrack) {
			const mainSegmentId = mediaEngine.index.segmentAt(desiredSeekTimeMs, mainTrack);
			if (mainSegmentId !== void 0 && mediaEngine.transport.isCached(mainSegmentId, mainTrack)) {
				this.#currentRenditionId = "main";
				return this.#getMainVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal);
			}
		}
		if (this.isInProductionRenderingMode()) {
			this.#currentRenditionId = "main";
			return this.#getMainVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal);
		}
		if (mediaEngine.tracks.scrub) {
			const scrubSample = await this.#getScrubVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal);
			if (scrubSample) {
				this.#currentRenditionId = "scrub";
				this.#maybeScheduleQualityUpgrade(mediaEngine, desiredSeekTimeMs);
				return scrubSample;
			}
		}
		this.#currentRenditionId = "main";
		return this.#getMainVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal);
	}
	/**
	* Get scrub (low-resolution) video sample for fast preview scrubbing.
	* Used in preview mode for faster response during timeline scrubbing.
	*/
	async #getScrubVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal) {
		const scrubTrack = mediaEngine.tracks.scrub;
		if (!scrubTrack) return;
		const segmentId = mediaEngine.index.segmentAt(desiredSeekTimeMs, scrubTrack);
		if (segmentId === void 0) return;
		const scrubInput = await scrubInputCache.getOrCreateInput(mediaEngine.src, segmentId, async () => {
			let initSegment;
			let mediaSegment;
			try {
				const initP = mediaEngine.transport.fetchInitSegment(scrubTrack, signal);
				const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, scrubTrack, signal);
				initP.catch(() => {});
				mediaP.catch(() => {});
				[initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
			} catch (error) {
				if (error instanceof DOMException && error.name === "AbortError") throw error;
				return;
			}
			if (!initSegment || !mediaSegment) return;
			signal.throwIfAborted();
			const combinedBlob = new Blob([initSegment, mediaSegment]);
			signal.throwIfAborted();
			const arrayBuffer = await combinedBlob.arrayBuffer();
			signal.throwIfAborted();
			const { BufferedSeekingInput } = await import("./EFMedia/BufferedSeekingInput.js");
			return new BufferedSeekingInput(arrayBuffer, {
				videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
				audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
				startTimeOffsetMs: scrubTrack.startTimeOffsetMs
			});
		});
		if (!scrubInput) return;
		signal.throwIfAborted();
		const videoTrack = await scrubInput.getFirstVideoTrack();
		if (!videoTrack) return;
		signal.throwIfAborted();
		return scrubInput.seek(videoTrack.id, desiredSeekTimeMs);
	}
	/**
	* Get main video sample for a given time.
	*/
	async #getMainVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal) {
		const videoTrack = mediaEngine.tracks.video;
		if (!videoTrack) return;
		const segmentId = mediaEngine.index.segmentAt(desiredSeekTimeMs, videoTrack);
		if (segmentId === void 0) return;
		const mainInput = await mainVideoInputCache.getOrCreateInput(mediaEngine.src, segmentId, String(videoTrack.id), async () => {
			let initSegment;
			let mediaSegment;
			try {
				const initP = mediaEngine.transport.fetchInitSegment(videoTrack, signal);
				const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, videoTrack, signal);
				initP.catch(() => {});
				mediaP.catch(() => {});
				[initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
			} catch (error) {
				if (error instanceof DOMException && error.name === "AbortError") throw error;
				if (error instanceof Error && (error.message.includes("401") || error.message.includes("UNAUTHORIZED") || error.message.includes("Failed to fetch") || error.message.includes("File not found") || error.message.includes("Media segment not found") || error.message.includes("Init segment not found") || error.message.includes("Track not found"))) return;
				throw error;
			}
			if (!initSegment || !mediaSegment) return;
			signal.throwIfAborted();
			const combinedBlob = new Blob([initSegment, mediaSegment]);
			signal.throwIfAborted();
			const arrayBuffer = await combinedBlob.arrayBuffer();
			signal.throwIfAborted();
			const { BufferedSeekingInput } = await import("./EFMedia/BufferedSeekingInput.js");
			return new BufferedSeekingInput(arrayBuffer, {
				videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
				audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
				startTimeOffsetMs: videoTrack.startTimeOffsetMs
			});
		});
		if (!mainInput) return;
		signal.throwIfAborted();
		const videoTrackInfo = await mainInput.getFirstVideoTrack();
		if (!videoTrackInfo) return;
		signal.throwIfAborted();
		return await mainInput.seek(videoTrackInfo.id, desiredSeekTimeMs);
	}
	/**
	* Delayed loading state manager for user feedback
	*/
	#delayedLoadingState;
	constructor() {
		super();
		this.canvasRef = createRef();
		this.unifiedVideoSeekTask = new VideoSeekTask();
		this.loadingState = {
			isLoading: false,
			operation: null,
			message: ""
		};
		this.#delayedLoadingState = new DelayedLoadingState(100, (isLoading, message) => {
			this.setLoadingState(isLoading, null, message);
		});
	}
	updated(changedProperties) {
		super.updated(changedProperties);
		if (changedProperties.has("src") || changedProperties.has("fileId")) {
			this.#invalidateUpgradeState("src-change");
			this.#prewarmQualityUpgrade();
		}
		if ([
			"_trimStartMs",
			"_trimEndMs",
			"_sourceInMs",
			"_sourceOutMs"
		].some((prop) => changedProperties.has(prop))) {
			this.#invalidateUpgradeState("bounds-change");
			this.#prewarmQualityUpgrade();
		}
	}
	/**
	* Eagerly load the media engine and pre-warm main-quality segments for the
	* start of this clip. Called when src/fileId or source bounds change so that
	* segments are already in cache by the time the element first becomes visible.
	*
	* Without pre-warming, quality upgrade only begins after the first scrub frame
	* is displayed, causing ~12 frames of blur at the cold-start of every clip.
	*/
	#prewarmQualityUpgrade() {
		if (this.isInProductionRenderingMode()) return;
		if (!this.src && !this.fileId) return;
		this.getMediaEngine().then((engine) => {
			if (!engine) return;
			const sourceInMs = this.sourceInMs ?? 0;
			this.#maybeScheduleQualityUpgrade(engine, sourceInMs);
		}).catch(() => {});
	}
	render() {
		return html`
      <canvas ${ref(this.canvasRef)}></canvas>
      ${this.loadingState.isLoading ? html`<div class="loading-overlay"><div class="loading-bar"></div></div>` : ""}
    `;
	}
	get canvasElement() {
		const referencedCanvas = this.canvasRef.value;
		if (referencedCanvas) return referencedCanvas;
		const shadowCanvas = this.shadowRoot?.querySelector("canvas");
		if (shadowCanvas) return shadowCanvas;
	}
	/**
	* Start a delayed loading operation for testing
	*/
	startDelayedLoading(operationId, message, options = {}) {
		this.#delayedLoadingState.startLoading(operationId, message, options);
	}
	/**
	* Clear a delayed loading operation for testing
	*/
	clearDelayedLoading(operationId) {
		this.#delayedLoadingState.clearLoading(operationId);
	}
	/**
	* Set loading state for user feedback
	*/
	setLoadingState(isLoading, operation = null, message = "") {
		this.loadingState = {
			isLoading,
			operation,
			message
		};
	}
	/**
	* Paint the current video frame to canvas
	*/
	paint(seekToMs, parentSpan) {
		const parentContext = parentSpan ? trace.setSpan(context.active(), parentSpan) : void 0;
		withSpanSync("video.paint", {
			elementId: this.id || "unknown",
			seekToMs,
			src: this.src || "none"
		}, parentContext, (span) => {
			const t0 = performance.now();
			const isProductionRendering = this.isInProductionRenderingMode();
			const t1 = performance.now();
			span.setAttribute("isProductionRendering", isProductionRendering);
			span.setAttribute("modeCheckMs", t1 - t0);
			try {
				const t2 = performance.now();
				const videoSample = this.#cachedVideoSample;
				span.setAttribute("hasVideoSample", !!videoSample);
				span.setAttribute("valueAccessMs", t2 - t1);
				if (videoSample) {
					const t3 = performance.now();
					const videoFrame = videoSample.toVideoFrame();
					const t4 = performance.now();
					span.setAttribute("toVideoFrameMs", t4 - t3);
					try {
						const t5 = performance.now();
						this.displayFrame(videoFrame, seekToMs, span);
						const t6 = performance.now();
						span.setAttribute("displayFrameMs", t6 - t5);
					} finally {
						videoFrame.close();
					}
				}
			} catch (error) {
				console.warn("Video pipeline error:", error);
			}
			if (!isProductionRendering) {} else {
				if (!this.rootTimegroup) {
					span.setAttribute("skipped", "no-root-timegroup");
					return;
				}
				if (!this.isFrameRenderingActive()) {
					span.setAttribute("skipped", "frame-rendering-not-active");
					return;
				}
			}
			const tEnd = performance.now();
			span.setAttribute("totalPaintMs", tEnd - t0);
		});
	}
	/**
	* Clear the canvas when element becomes inactive
	*/
	clearCanvas() {
		if (!this.canvasElement) return;
		const ctx = this.canvasElement.getContext("2d", { willReadFrequently: true });
		if (ctx) ctx.clearRect(0, 0, this.canvasElement.width, this.canvasElement.height);
	}
	/**
	* Display a video frame on the canvas
	*/
	displayFrame(frame, seekToMs, parentSpan) {
		const parentContext = parentSpan ? trace.setSpan(context.active(), parentSpan) : void 0;
		withSpanSync("video.displayFrame", {
			elementId: this.id || "unknown",
			seekToMs,
			format: frame.format || "unknown",
			width: frame.codedWidth,
			height: frame.codedHeight
		}, parentContext, (span) => {
			const t0 = performance.now();
			log("trace: displayFrame start", {
				seekToMs,
				frameFormat: frame.format
			});
			if (!this.canvasElement) {
				log("trace: displayFrame aborted - no canvas element");
				throw new Error(`Frame display failed: Canvas element is not available at time ${seekToMs}ms. The video component may not be properly initialized.`);
			}
			const t1 = performance.now();
			span.setAttribute("getCanvasMs", Math.round((t1 - t0) * 100) / 100);
			const ctx = this.canvasElement.getContext("2d", { willReadFrequently: true });
			const t2 = performance.now();
			span.setAttribute("getCtxMs", Math.round((t2 - t1) * 100) / 100);
			if (!ctx) {
				log("trace: displayFrame aborted - no canvas context");
				throw new Error(`Frame display failed: Unable to get 2D canvas context at time ${seekToMs}ms. This may indicate a browser compatibility issue or canvas corruption.`);
			}
			const frameWidth = frame.displayWidth;
			const frameHeight = frame.displayHeight;
			let resized = false;
			if (frameWidth && frameHeight) {
				if (frameWidth > this.canvasElement.width || frameHeight > this.canvasElement.height) {
					const newWidth = Math.max(this.canvasElement.width, frameWidth);
					const newHeight = Math.max(this.canvasElement.height, frameHeight);
					log("trace: updating canvas dimensions", {
						width: newWidth,
						height: newHeight
					});
					this.canvasElement.width = newWidth;
					this.canvasElement.height = newHeight;
					resized = true;
					const t3 = performance.now();
					span.setAttribute("resizeMs", Math.round((t3 - t2) * 100) / 100);
				}
			}
			span.setAttribute("canvasResized", resized);
			if (frame.format === null) {
				log("trace: displayFrame aborted - null frame format");
				throw new Error(`Frame display failed: Video frame has null format at time ${seekToMs}ms. This indicates corrupted or incompatible video data.`);
			}
			const tDrawStart = performance.now();
			ctx.drawImage(frame, 0, 0, this.canvasElement.width, this.canvasElement.height);
			const tDrawEnd = performance.now();
			span.setAttribute("drawImageMs", Math.round((tDrawEnd - tDrawStart) * 100) / 100);
			span.setAttribute("totalDisplayMs", Math.round((tDrawEnd - t0) * 100) / 100);
			span.setAttribute("canvasWidth", this.canvasElement.width);
			span.setAttribute("canvasHeight", this.canvasElement.height);
			log("trace: frame drawn to canvas", { seekToMs });
		});
	}
	/**
	* Check if we're in production rendering mode (EF_FRAMEGEN active) vs preview mode
	*/
	isInProductionRenderingMode() {
		if (typeof window.EF_RENDERING === "function") return window.EF_RENDERING();
		if (document.querySelector("ef-workbench")?.rendering) return true;
		if (window.EF_FRAMEGEN?.renderOptions) return true;
		return false;
	}
	/**
	* Check if EF_FRAMEGEN has explicitly started frame rendering (not just initialization)
	*/
	isFrameRenderingActive() {
		if (!window.EF_FRAMEGEN?.renderOptions) return false;
		const renderStartTime = window.EF_FRAMEGEN.renderOptions.encoderOptions.fromMs;
		return (this.rootTimegroup?.currentTimeMs || 0) >= renderStartTime;
	}
	/**
	* Get a decoded VideoFrame at a specific source media timestamp.
	* Returns a standard WebCodecs VideoFrame — caller MUST call .close() when done.
	*
	* Uses the same routing logic as the unified video system:
	* - "auto": main track for production rendering, follows normal routing otherwise
	* - "scrub": force low-res scrub track (for thumbnails)
	* - "main": force full-quality main track
	*
	* @param sourceTimeMs - Timestamp in source media coordinates (not timeline)
	* @param options - Quality and abort signal
	* @returns VideoFrame that the caller must close()
	* @public
	*/
	async getVideoFrameAtSourceTime(sourceTimeMs, options = {}) {
		const { quality = "auto", signal: providedSignal } = options;
		const signal = providedSignal ?? new AbortController().signal;
		signal.throwIfAborted();
		this.playbackController?.suspendSelfRender();
		try {
			const mediaEngine = await this.getMediaEngine(signal);
			signal.throwIfAborted();
			if (!mediaEngine) throw new Error("No media engine available for frame capture");
			const useMainTrack = quality === "main" || quality === "auto" && this.isInProductionRenderingMode();
			let videoSample;
			const { BufferedSeekingInput } = await import("./EFMedia/BufferedSeekingInput.js");
			signal.throwIfAborted();
			if (useMainTrack) {
				const videoTrack = mediaEngine.tracks.video;
				if (!videoTrack) throw new Error("No video rendition available");
				const segmentId = mediaEngine.index.segmentAt(sourceTimeMs, videoTrack);
				if (segmentId === void 0) throw new Error(`Cannot compute segment ID for time ${sourceTimeMs}ms`);
				const seekingInput = await mainVideoInputCache.getOrCreateInput(mediaEngine.src, segmentId, String(videoTrack.id), async () => {
					const initP = mediaEngine.transport.fetchInitSegment(videoTrack, signal);
					const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, videoTrack, signal);
					initP.catch(() => {});
					mediaP.catch(() => {});
					const [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
					if (!initSegment || !mediaSegment) return;
					return new BufferedSeekingInput(await new Blob([initSegment, mediaSegment]).arrayBuffer(), {
						videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
						audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
						startTimeOffsetMs: videoTrack.startTimeOffsetMs
					});
				});
				signal.throwIfAborted();
				if (!seekingInput) throw new Error(`Failed to fetch video segments for time ${sourceTimeMs}ms`);
				const seekingVideoTrack = await seekingInput.getFirstVideoTrack();
				signal.throwIfAborted();
				if (!seekingVideoTrack) throw new Error("No video track found in segment");
				videoSample = await seekingInput.seek(seekingVideoTrack.id, sourceTimeMs);
				signal.throwIfAborted();
			} else {
				const scrubTrack = mediaEngine.tracks.scrub;
				if (!scrubTrack) return this.getVideoFrameAtSourceTime(sourceTimeMs, {
					quality: "main",
					signal
				});
				const segmentId = mediaEngine.index.segmentAt(sourceTimeMs, scrubTrack);
				if (segmentId === void 0) throw new Error(`Cannot compute scrub segment ID for time ${sourceTimeMs}ms`);
				const seekingInput = await scrubInputCache.getOrCreateInput(mediaEngine.src, segmentId, async () => {
					const initP = mediaEngine.transport.fetchInitSegment(scrubTrack, signal);
					const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, scrubTrack, signal);
					initP.catch(() => {});
					mediaP.catch(() => {});
					const [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
					if (!initSegment || !mediaSegment) return;
					return new BufferedSeekingInput(await new Blob([initSegment, mediaSegment]).arrayBuffer(), {
						videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
						audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
						startTimeOffsetMs: scrubTrack.startTimeOffsetMs
					});
				});
				signal.throwIfAborted();
				if (!seekingInput) return this.getVideoFrameAtSourceTime(sourceTimeMs, {
					quality: "main",
					signal
				});
				const seekingVideoTrack = await seekingInput.getFirstVideoTrack();
				signal.throwIfAborted();
				if (!seekingVideoTrack) return this.getVideoFrameAtSourceTime(sourceTimeMs, {
					quality: "main",
					signal
				});
				videoSample = await seekingInput.seek(seekingVideoTrack.id, sourceTimeMs);
				signal.throwIfAborted();
			}
			if (!videoSample) throw new Error(`No video sample found at ${sourceTimeMs}ms`);
			return videoSample.toVideoFrame();
		} finally {
			this.playbackController?.resumeSelfRender();
		}
	}
	/**
	* Capture a video frame directly at a source media timestamp.
	* Designed for export/rendering.
	* Does NOT paint to the element's internal canvas.
	*
	* Uses the same routing logic as unified video system:
	* - "auto": main track for production rendering, follows normal routing otherwise
	* - "scrub": force low-res scrub track (for thumbnails)
	* - "main": force full-quality main track
	*
	* @param sourceTimeMs - Timestamp in source media coordinates (not timeline)
	* @param options - Capture options including quality and abort signal
	* @returns Frame data for serialization
	* @public
	*/
	async captureFrameAtSourceTime(sourceTimeMs, options = {}) {
		const videoFrame = await this.getVideoFrameAtSourceTime(sourceTimeMs, options);
		try {
			options.signal?.throwIfAborted();
			const canvas = new OffscreenCanvas(videoFrame.codedWidth, videoFrame.codedHeight);
			const ctx = canvas.getContext("2d");
			if (!ctx) throw new Error("Failed to get 2d context from OffscreenCanvas");
			ctx.drawImage(videoFrame, 0, 0);
			options.signal?.throwIfAborted();
			const blob = await canvas.convertToBlob({
				type: "image/jpeg",
				quality: .92
			});
			options.signal?.throwIfAborted();
			const dataUrl = await new Promise((resolve, reject) => {
				const reader = new FileReader();
				reader.onload = () => resolve(reader.result);
				reader.onerror = reject;
				reader.readAsDataURL(blob);
			});
			options.signal?.throwIfAborted();
			return {
				dataUrl,
				width: videoFrame.codedWidth,
				height: videoFrame.codedHeight
			};
		} finally {
			videoFrame.close();
		}
	}
	/**
	* Pre-fetch scrub segments for given timestamps.
	* Loads 30-second segments sequentially, emitting progress events.
	* This ensures scrub track is cached for fast thumbnail generation.
	*
	* @param timestamps - Array of timestamps (in ms) that will be captured
	* @param onProgress - Optional callback for loading progress
	* @param signal - Optional AbortSignal for cancellation
	* @returns Promise that resolves when all segments are cached
	* @public
	*/
	async prefetchScrubSegments(timestamps, onProgress, signal) {
		const mediaEngine = await this.getMediaEngine(signal);
		if (!mediaEngine) {
			log("prefetchScrubSegments: no media engine available");
			return;
		}
		const scrubTrack = mediaEngine.tracks.scrub;
		if (!scrubTrack) {
			log("prefetchScrubSegments: no scrub rendition available");
			return;
		}
		const segmentIds = /* @__PURE__ */ new Set();
		for (const ts of timestamps) {
			const segmentId = mediaEngine.index.segmentAt(ts, scrubTrack);
			if (segmentId !== void 0) segmentIds.add(segmentId);
		}
		if (segmentIds.size === 0) {
			log("prefetchScrubSegments: no segments to prefetch");
			return;
		}
		const firstSegmentId = Array.from(segmentIds)[0];
		if (mediaEngine.transport.isCached(firstSegmentId, scrubTrack)) {
			log("prefetchScrubSegments: scrub track already cached");
			return;
		}
		log(`prefetchScrubSegments: fetching scrub track for ${segmentIds.size} segments...`);
		const durationMs = mediaEngine.durationMs || 0;
		this.dispatchEvent(new CustomEvent("scrub-segment-loading", {
			detail: {
				segmentId: 0,
				timeRangeMs: [0, durationMs],
				loaded: 0,
				total: 1,
				status: "loading"
			},
			bubbles: true,
			composed: true
		}));
		const fetchSignal = signal ?? new AbortController().signal;
		try {
			await mediaEngine.transport.fetchMediaSegment(firstSegmentId, scrubTrack, fetchSignal);
			log(`prefetchScrubSegments: scrub track loaded`);
		} catch (error) {
			log(`prefetchScrubSegments: failed to load scrub track`, error);
		}
		this.dispatchEvent(new CustomEvent("scrub-segment-loading", {
			detail: {
				segmentId: 0,
				timeRangeMs: [0, durationMs],
				loaded: 1,
				total: 1,
				status: "loaded"
			},
			bubbles: true,
			composed: true
		}));
		onProgress?.(1, 1, [0, durationMs]);
		log(`prefetchScrubSegments: complete`);
	}
	/**
	* Maybe schedule quality upgrade tasks for this element.
	* Called when returning a scrub sample - checks if state has changed and submits tasks.
	*/
	#maybeScheduleQualityUpgrade(mediaEngine, sourceTimeMs) {
		if (this.#renderingToVideo) return;
		const mainTrack = mediaEngine.tracks.video;
		if (!mainTrack) return;
		const segmentId = mediaEngine.index.segmentAt(sourceTimeMs, mainTrack);
		if (segmentId === void 0) return;
		const startTimeMs = this.startTimeMs;
		if (!(this.#upgradeState === null || this.#upgradeState.segmentId !== segmentId || this.#upgradeState.startTimeMs !== startTimeMs)) {
			const currentTaskKey = `${this.#upgradeOwnerId}:${segmentId}:${mainTrack.id}`;
			const scheduler$1 = this.rootTimegroup?.qualityUpgradeScheduler;
			if (scheduler$1?.isActive(currentTaskKey) || scheduler$1?.isPending(currentTaskKey)) return;
			this.rootTimegroup?.qualityUpgradeScheduler?.cancelForOwner(this.#upgradeOwnerId);
			this.#upgradeState = null;
		}
		const segments = this.#computeLookaheadSegments(mediaEngine, sourceTimeMs, mainTrack);
		if (segments.length === 0) return;
		const tasks = segments.map((seg) => ({
			key: `${this.#upgradeOwnerId}:${seg.segmentId}:${mainTrack.id}`,
			fetch: async (signal) => {
				await mediaEngine.transport.fetchInitSegment(mainTrack, signal);
				await mediaEngine.transport.fetchMediaSegment(seg.segmentId, mainTrack, signal);
			},
			deadlineMs: seg.deadlineMs,
			owner: this.#upgradeOwnerId
		}));
		const scheduler = this.rootTimegroup?.qualityUpgradeScheduler;
		if (scheduler) scheduler.replaceForOwner(this.#upgradeOwnerId, tasks);
		else this.#fetchStandalone(tasks);
		this.#upgradeState = {
			sourceTimeMs,
			segmentId,
			startTimeMs,
			submittedKeys: new Set(tasks.map((t) => t.key))
		};
	}
	/**
	* Compute lookahead segments with deadlines in timeline space.
	*/
	#computeLookaheadSegments(mediaEngine, currentSourceTimeMs, track, maxLookahead = 5) {
		const results = [];
		const playheadMs = this.rootTimegroup?.currentTimeMs ?? 0;
		const seen = /* @__PURE__ */ new Set();
		let probeTimeMs = currentSourceTimeMs;
		while (seen.size < maxLookahead) {
			const segmentId = mediaEngine.index.segmentAt(probeTimeMs, track);
			if (segmentId === void 0) break;
			if (seen.has(segmentId)) break;
			seen.add(segmentId);
			if (!mediaEngine.transport.isCached(segmentId, track)) {
				const deadlineMs = playheadMs + (probeTimeMs - currentSourceTimeMs);
				results.push({
					segmentId,
					deadlineMs
				});
			}
			const thisDuration = track.segmentDurationsMs?.[segmentId - 1] ?? track.segmentDurationMs ?? 2e3;
			probeTimeMs += thisDuration;
		}
		return results;
	}
	/**
	* Standalone mode: fetch tasks sequentially without scheduler.
	*/
	#fetchStandalone(tasks) {
		this.#standaloneUpgradeController?.abort();
		this.#standaloneUpgradeController = new AbortController();
		const signal = this.#standaloneUpgradeController.signal;
		(async () => {
			for (const task of tasks) {
				if (signal.aborted) break;
				try {
					await task.fetch(signal);
				} catch {}
			}
			if (!signal.aborted) this.playbackController?.runThrottledFrameTask().catch(() => {});
		})().catch(() => {});
	}
	/**
	* Invalidate upgrade state and optionally cancel queued tasks.
	*/
	#invalidateUpgradeState(reason) {
		if (reason === "src-change" || reason === "disconnect") this.rootTimegroup?.qualityUpgradeScheduler?.cancelForOwner(this.#upgradeOwnerId);
		this.#upgradeState = null;
	}
	/**
	* Clean up resources when component is disconnected
	*/
	disconnectedCallback() {
		super.disconnectedCallback();
		this.#delayedLoadingState.clearAllLoading();
		this.#invalidateUpgradeState("disconnect");
		this.#standaloneUpgradeController?.abort();
		this.#standaloneUpgradeController = null;
	}
	didBecomeRoot() {
		super.didBecomeRoot();
	}
	didBecomeChild() {
		super.didBecomeChild();
	}
	/**
	* Get the natural dimensions of the video (coded width and height).
	* Returns null if the video hasn't loaded yet or canvas isn't available.
	*
	* @public
	*/
	getNaturalDimensions() {
		const canvas = this.canvasElement;
		if (!canvas || canvas.width === 0 || canvas.height === 0) return null;
		return {
			width: canvas.width,
			height: canvas.height
		};
	}
	/**
	* Render this video element to an MP4 using the direct video-to-video fast path.
	* Bypasses DOM serialization — decodes frames directly and re-encodes to MP4.
	* Respects trim, CSS filter, and opacity.
	*
	* @param options - Rendering options (fps, codec, bitrate, etc.)
	* @returns Promise resolving to video buffer (if returnBuffer), or undefined
	* @public
	*/
	async renderToVideo(options) {
		this.#renderingToVideo = true;
		try {
			const { renderVideoToVideo } = await import("../preview/renderVideoToVideo.js");
			return await renderVideoToVideo(this, options);
		} finally {
			this.#renderingToVideo = false;
		}
	}
};
__decorate([state()], EFVideo.prototype, "loadingState", void 0);
EFVideo = __decorate([customElement("ef-video")], EFVideo);

//#endregion
export { EFVideo };
//# sourceMappingURL=EFVideo.js.map