import { withSpan } from "../../otel/tracingHelpers.js";
import { roundToMilliseconds } from "./shared/PrecisionUtils.js";
import { DEFAULT_MEDIABUNNY_TIMEOUT_MS, withTimeout } from "./shared/timeoutUtils.js";
import { SampleBuffer } from "../SampleBuffer.js";
import { AudioSampleSink, BufferSource, Input, InputAudioTrack, InputVideoTrack, MP4, VideoSampleSink } from "mediabunny";

//#region src/elements/EFMedia/BufferedSeekingInput.ts
const defaultOptions = {
	videoBufferSize: 30,
	audioBufferSize: 100,
	startTimeOffsetMs: 0
};
var NoSample = class extends RangeError {};
var BufferedSeekingInput = class {
	constructor(arrayBuffer, options) {
		this.trackIterators = /* @__PURE__ */ new Map();
		this.trackBuffers = /* @__PURE__ */ new Map();
		this.trackIteratorCreationPromises = /* @__PURE__ */ new Map();
		this.trackSeekPromises = /* @__PURE__ */ new Map();
		this.input = new Input({
			source: new BufferSource(arrayBuffer),
			formats: [MP4]
		});
		this.options = {
			...defaultOptions,
			...options
		};
		this.startTimeOffsetMs = this.options.startTimeOffsetMs ?? 0;
	}
	getBufferSize(trackId) {
		const buffer = this.trackBuffers.get(trackId);
		return buffer ? buffer.length : 0;
	}
	getBufferContents(trackId) {
		const buffer = this.trackBuffers.get(trackId);
		return buffer ? Object.freeze([...buffer.getContents()]) : [];
	}
	getBufferTimestamps(trackId) {
		return this.getBufferContents(trackId).map((sample) => sample.timestamp || 0);
	}
	clearBuffer(trackId) {
		const buffer = this.trackBuffers.get(trackId);
		if (buffer) buffer.clear();
	}
	computeDuration() {
		return this.input.computeDuration();
	}
	async getTrack(trackId) {
		const track = (await withTimeout(this.input.getTracks(), 5e3, "BufferedSeekingInput.getTracks")).find((track$1) => track$1.id === trackId);
		if (!track) throw new Error(`Track ${trackId} not found`);
		return track;
	}
	async getAudioTrack(trackId) {
		const track = (await withTimeout(this.input.getAudioTracks(), 5e3, "BufferedSeekingInput.getAudioTracks")).find((track$1) => track$1.id === trackId && track$1.type === "audio");
		if (!track) throw new Error(`Track ${trackId} not found`);
		return track;
	}
	async getVideoTrack(trackId) {
		const track = (await withTimeout(this.input.getVideoTracks(), 5e3, "BufferedSeekingInput.getVideoTracks")).find((track$1) => track$1.id === trackId && track$1.type === "video");
		if (!track) throw new Error(`Track ${trackId} not found`);
		return track;
	}
	async getFirstVideoTrack() {
		return (await withTimeout(this.input.getVideoTracks(), 5e3, "BufferedSeekingInput.getFirstVideoTrack"))[0];
	}
	async getFirstAudioTrack() {
		return (await withTimeout(this.input.getAudioTracks(), 5e3, "BufferedSeekingInput.getFirstAudioTrack"))[0];
	}
	getTrackIterator(track) {
		if (this.trackIterators.has(track.id)) return this.trackIterators.get(track.id);
		const trackIterator = this.createTrackIterator(track);
		this.trackIterators.set(track.id, trackIterator);
		return trackIterator;
	}
	createTrackSampleSink(track) {
		if (track instanceof InputAudioTrack) return new AudioSampleSink(track);
		if (track instanceof InputVideoTrack) return new VideoSampleSink(track);
		throw new Error(`Unsupported track type ${track.type}`);
	}
	createTrackIterator(track) {
		return this.createTrackSampleSink(track).samples();
	}
	createTrackBuffer(track) {
		if (track.type === "audio") {
			const bufferSize$1 = this.options.audioBufferSize;
			return new SampleBuffer(bufferSize$1);
		}
		const bufferSize = this.options.videoBufferSize;
		return new SampleBuffer(bufferSize);
	}
	getTrackBuffer(track) {
		const maybeTrackBuffer = this.trackBuffers.get(track.id);
		if (maybeTrackBuffer) return maybeTrackBuffer;
		const trackBuffer = this.createTrackBuffer(track);
		this.trackBuffers.set(track.id, trackBuffer);
		return trackBuffer;
	}
	async seek(trackId, timeMs) {
		return withSpan("bufferedInput.seek", {
			trackId,
			timeMs,
			startTimeOffsetMs: this.startTimeOffsetMs
		}, void 0, async (span) => {
			const roundedMediaTimeMs = roundToMilliseconds(timeMs + this.startTimeOffsetMs);
			span.setAttribute("roundedMediaTimeMs", roundedMediaTimeMs);
			const existingSeek = this.trackSeekPromises.get(trackId);
			if (existingSeek) {
				span.setAttribute("waitedForExistingSeek", true);
				await existingSeek;
			}
			const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);
			this.trackSeekPromises.set(trackId, seekPromise);
			try {
				return await seekPromise;
			} finally {
				this.trackSeekPromises.delete(trackId);
			}
		});
	}
	async resetIterator(track) {
		this.trackBuffers.get(track.id)?.clear();
		const ongoingIteratorCreation = this.trackIteratorCreationPromises.get(track.id);
		if (ongoingIteratorCreation) await ongoingIteratorCreation;
		const iterator = this.trackIterators.get(track.id);
		if (iterator) try {
			await iterator.return?.();
		} catch (_error) {}
		this.trackIterators.delete(track.id);
	}
	#seekLock;
	async seekSafe(trackId, timeMs) {
		return withSpan("bufferedInput.seekSafe", {
			trackId,
			timeMs
		}, void 0, async (span) => {
			if (this.#seekLock) {
				span.setAttribute("waitedForSeekLock", true);
				await this.#seekLock.promise;
			}
			const seekLock = Promise.withResolvers();
			this.#seekLock = seekLock;
			try {
				const track = await this.getTrack(trackId);
				span.setAttribute("trackType", track.type);
				const trackBuffer = this.getTrackBuffer(track);
				const roundedTimeMs = roundToMilliseconds(timeMs);
				const timeoutMs = 5e3;
				const firstTimestampMs = roundToMilliseconds(await Promise.race([track.getFirstTimestamp(), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`getFirstTimestamp timeout after ${timeoutMs}ms`)), timeoutMs))]) * 1e3);
				span.setAttribute("firstTimestampMs", firstTimestampMs);
				if (roundedTimeMs < firstTimestampMs - .01) {
					console.error(`[BufferedSeekingInput.seekSafe] OUT_OF_BOUNDS trackId=${trackId} roundedTimeMs=${roundedTimeMs} firstTimestampMs=${firstTimestampMs}`);
					throw new NoSample(`Seeking outside bounds of input ${roundedTimeMs} < ${firstTimestampMs}`);
				}
				const bufferContents = trackBuffer.getContents();
				span.setAttribute("bufferContentsLength", bufferContents.length);
				if (bufferContents.length > 0) {
					const bufferStartMs = roundToMilliseconds(trackBuffer.firstTimestamp * 1e3);
					span.setAttribute("bufferStartMs", bufferStartMs);
					if (roundedTimeMs < bufferStartMs) {
						span.setAttribute("resetIterator", true);
						await this.resetIterator(track);
					}
				}
				const alreadyInBuffer = trackBuffer.find(timeMs);
				if (alreadyInBuffer) {
					span.setAttribute("foundInBuffer", true);
					span.setAttribute("bufferSize", trackBuffer.length);
					const contents$1 = trackBuffer.getContents();
					if (contents$1.length > 0) span.setAttribute("bufferTimestamps", contents$1.map((s) => Math.round((s.timestamp || 0) * 1e3)).slice(0, 10).join(","));
					return alreadyInBuffer;
				}
				span.setAttribute("foundInBuffer", false);
				span.setAttribute("bufferSize", trackBuffer.length);
				span.setAttribute("requestedTimeMs", Math.round(timeMs));
				const contents = trackBuffer.getContents();
				if (contents.length > 0) {
					const firstSample = contents[0];
					const lastSample = contents[contents.length - 1];
					if (firstSample && lastSample) {
						const bufferStartMs = Math.round((firstSample.timestamp || 0) * 1e3);
						const bufferEndMs = Math.round(((lastSample.timestamp || 0) + (lastSample.duration || 0)) * 1e3);
						span.setAttribute("bufferStartMs", bufferStartMs);
						span.setAttribute("bufferEndMs", bufferEndMs);
						span.setAttribute("bufferRangeMs", `${bufferStartMs}-${bufferEndMs}`);
					}
				}
				const iterator = this.getTrackIterator(track);
				let iterationCount = 0;
				const decodeStart = performance.now();
				while (true) {
					iterationCount++;
					const iterStart = performance.now();
					const { done, value: decodedSample } = await withTimeout(iterator.next(), DEFAULT_MEDIABUNNY_TIMEOUT_MS, `iterator.next() for ${track.type} track ${trackId} iteration ${iterationCount}`);
					const iterEnd = performance.now();
					if (iterationCount <= 5) span.setAttribute(`iter${iterationCount}Ms`, Math.round((iterEnd - iterStart) * 100) / 100);
					if (decodedSample) {
						trackBuffer.push(decodedSample);
						if (iterationCount <= 5) span.setAttribute(`iter${iterationCount}Timestamp`, Math.round((decodedSample.timestamp || 0) * 1e3));
					}
					const foundSample = trackBuffer.find(roundedTimeMs);
					if (foundSample) {
						const decodeEnd = performance.now();
						span.setAttribute("iterationCount", iterationCount);
						span.setAttribute("decodeMs", Math.round((decodeEnd - decodeStart) * 100) / 100);
						span.setAttribute("avgIterMs", Math.round((decodeEnd - decodeStart) / iterationCount * 100) / 100);
						span.setAttribute("foundSample", true);
						span.setAttribute("foundTimestamp", Math.round((foundSample.timestamp || 0) * 1e3));
						return foundSample;
					}
					if (done) break;
				}
				span.setAttribute("iterationCount", iterationCount);
				span.setAttribute("reachedEnd", true);
				const finalBufferContents = trackBuffer.getContents();
				if (finalBufferContents.length > 0) {
					const lastSample = finalBufferContents[finalBufferContents.length - 1];
					const lastSampleEndMs = roundToMilliseconds(((lastSample?.timestamp || 0) + (lastSample?.duration || 0)) * 1e3);
					const trackDurationMs = await track.computeDuration() * 1e3;
					const isSeekingToTrackEnd = roundToMilliseconds(timeMs) === roundToMilliseconds(trackDurationMs);
					const isAtEndOfTrack = roundToMilliseconds(timeMs) >= lastSampleEndMs;
					if (isSeekingToTrackEnd && isAtEndOfTrack) {
						span.setAttribute("returnedLastSample", true);
						return lastSample;
					}
				}
				throw new NoSample(`Sample not found for time ${timeMs} in ${track.type} track ${trackId}`);
			} finally {
				this.#seekLock = void 0;
				seekLock.resolve();
			}
		});
	}
};

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