/*!
 * Copyright (c) 2026-present, Vanilagy and contributors
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

import { TrackType } from '../output';
import { SAMPLES_PER_AAC_FRAME } from '../adts/adts-demuxer';
import { MAX_ADTS_FRAME_HEADER_SIZE, readAdtsFrameHeader } from '../adts/adts-reader';
import { aacChannelMap, aacFrequencyTable } from '../../shared/aac-misc';
import {
	AacCodecInfo,
	AudioCodec,
	extractAudioCodecString,
	extractVideoCodecString,
	MediaCodec,
	VideoCodec,
} from '../codec';
import {
	AC3_ACMOD_CHANNEL_COUNTS,
	AC3_SAMPLES_PER_FRAME,
	AvcDecoderConfigurationRecord,
	AvcNalUnitType,
	determineVideoPacketType,
	extractAvcDecoderConfigurationRecord,
	extractHevcDecoderConfigurationRecord,
	extractNalUnitTypeForAvc,
	extractNalUnitTypeForHevc,
	EAC3_NUMBLKS_TABLE,
	getEac3ChannelCount,
	getEac3SampleRate,
	HevcDecoderConfigurationRecord,
	HevcNalUnitType,
	parseAc3SyncFrame,
	parseAvcSps,
	parseEac3SyncFrame,
	parseHevcSps,
	AC3_FRAME_SIZES,
} from '../codec-data';
import { Demuxer } from '../demuxer';
import { Input } from '../input';
import {
	InputAudioTrackBacking,
	InputTrackBacking,
	InputVideoTrackBacking,
} from '../input-track';
import { PacketRetrievalOptions } from '../media-sink';
import { DEFAULT_TRACK_DISPOSITION, MetadataTags, TrackDisposition } from '../metadata';
import {
	assert,
	binarySearchExact,
	binarySearchLessOrEqual,
	COLOR_PRIMARIES_MAP_INVERSE,
	findLastIndex,
	floorToMultiple,
	last,
	MATRIX_COEFFICIENTS_MAP_INVERSE,
	Rotation,
	roundIfAlmostInteger,
	toDataView,
	TRANSFER_CHARACTERISTICS_MAP_INVERSE,
	UNDETERMINED_LANGUAGE,
} from '../misc';
import {
	MP3_FRAME_HEADER_SIZE,
	getMp3ChannelCount,
	readMp3FrameHeader,
} from '../../shared/mp3-misc';
import { EncodedPacket, PacketType, PLACEHOLDER_DATA } from '../packet';
import { FileSlice, readBytes, Reader, readU16Be, readU32Be, readU8 } from '../reader';
import { buildMpegTsMimeType, MpegTsStreamType, TIMESCALE, TS_PACKET_SIZE } from './mpeg-ts-misc';
import { AC3_SAMPLE_RATES } from '../../shared/ac3-misc';
import { Bitstream } from '../../shared/bitstream';

// Resources:
// ISO/IEC 13818-1

const MISSING_PTS_ERROR_MESSAGE = 'PES packet is missing PTS where it was expected. PES packets without PTS are not'
	+ ' currently supported. If you think this file should be supported, please report it.';

type ElementaryStream = {
	demuxer: MpegTsDemuxer;
	pid: number;
	streamType: number;
	initialized: boolean;
	firstSection: Section | null;
	/**
	 * Some muxers suck ass and don't correctly label key frames, meaning we'll need to use our skill to
	 * compensate for another programmer's skill issue.
	 */
	canBeTrustedWithKeyPackets: boolean;
	info: {
		type: 'video';
		codec: VideoCodec;
		decoderConfig: VideoDecoderConfig | null;
		avcCodecInfo: AvcDecoderConfigurationRecord | null;
		hevcCodecInfo: HevcDecoderConfigurationRecord | null;
		colorSpace: VideoColorSpaceInit;
		width: number;
		height: number;
		squarePixelWidth: number;
		squarePixelHeight: number;
		reorderSize: number;
	} | {
		type: 'audio';
		codec: AudioCodec;
		decoderConfig: AudioDecoderConfig | null;
		aacCodecInfo: AacCodecInfo | null;
		numberOfChannels: number;
		sampleRate: number;
	};
	/**
	 * Reference PES packets, spread throughout the file, to be used to speed up repeated random access. Sorted by both
	 * byte offset and PTS.
	 */
	referencePesPackets: TimestampedPesPacketHeader[];
};

type ElementaryVideoStream = ElementaryStream & { info: { type: 'video' } };
type ElementaryAudioStream = ElementaryStream & { info: { type: 'audio' } };

type TsPacketHeader = {
	payloadUnitStartIndicator: number;
	pid: number;
	adaptationFieldControl: number;
};

type TsPacket = TsPacketHeader & {
	body: Uint8Array<ArrayBufferLike>;
};

type Section = {
	startPos: number;
	endPos: number | null; // null if the section was not read fully
	pid: number;
	payload: Uint8Array<ArrayBufferLike>;
	randomAccessIndicator: number;
};

// Remember them so the warning doesn't get spammed
const ignoredStreamTypes = new Set<number>();

export class MpegTsDemuxer extends Demuxer {
	reader: Reader;

	metadataPromise: Promise<void> | null = null;
	elementaryStreams: ElementaryStream[] = [];
	trackBackingEntries: InputTrackBacking[] = [];
	packetOffset = 0;
	packetStride = -1;
	sectionEndPositions: number[] = [];
	seekChunkSize = 5 * 1024 * 1024; // 5 MiB, picked because most HLS segments are below this size
	minReferencePointByteDistance = -1;

	constructor(input: Input) {
		super(input);

		this.reader = input._reader;
	}

	async readMetadata() {
		return this.metadataPromise ??= (async () => {
			const lengthToCheck = TS_PACKET_SIZE + 16 + 1;
			let startingSlice = this.reader.requestSlice(0, lengthToCheck);
			if (startingSlice instanceof Promise) startingSlice = await startingSlice;
			assert(startingSlice);

			const startingBytes = readBytes(startingSlice, lengthToCheck);

			if (startingBytes[0] === 0x47 && startingBytes[TS_PACKET_SIZE] === 0x47) {
				// Regular MPEG-TS
				this.packetOffset = 0;
				this.packetStride = TS_PACKET_SIZE;
			} else if (startingBytes[0] === 0x47 && startingBytes[TS_PACKET_SIZE + 16] === 0x47) {
				// MPEG-TS with Forward Error Correction
				this.packetOffset = 0;
				this.packetStride = TS_PACKET_SIZE + 16;
			} else if (startingBytes[4] === 0x47 && startingBytes[4 + TS_PACKET_SIZE + 4] === 0x47) {
				// MPEG-2-TS (DVHS)
				this.packetOffset = 4;
				this.packetStride = TS_PACKET_SIZE + 4;
			} else {
				throw new Error('Unreachable.');
			}

			const MIN_REFERENCE_POINT_PACKET_DISTANCE = 256;
			this.minReferencePointByteDistance = MIN_REFERENCE_POINT_PACKET_DISTANCE * this.packetStride;

			let currentPos = this.packetOffset;
			let programMapPid: number | null = null;
			// Some files contain these multiple times, but we only care about their first appearance
			let hasProgramAssociationTable = false;
			let hasProgramMap = false;

			while (true) {
				const packetHeader = await this.readPacketHeader(currentPos);
				if (!packetHeader) {
					break;
				}

				if (packetHeader.payloadUnitStartIndicator === 0) {
					// Not the start of a section
					currentPos += this.packetStride;
					continue;
				}

				const section = await this.readSection(
					currentPos,
					true,
					!hasProgramMap, // Expect contiguous sections as long as we don't have the PMT
				);
				if (!section) {
					break;
				}

				const BYTES_BEFORE_SECTION_LENGTH = 3;
				const BITS_IN_CRC_32 = 32; // Duh

				// Some streams don't contain a PAT for some reason, so we must do some guesswork to figure out where
				// the PMT is.
				let isProbablyProgramMap = false;
				if (!hasProgramMap && section.pid !== 0) {
					const isPesPacket
						= section.payload[0] === 0x00 && section.payload[1] === 0x00 && section.payload[2] === 0x01;

					if (!isPesPacket) {
						// Assume it's a PSI

						const bitstream = new Bitstream(section.payload);
						const pointerField = bitstream.readAlignedByte();

						bitstream.skipBits(8 * pointerField);

						const tableId = bitstream.readBits(8);
						isProbablyProgramMap = tableId === 0x02; // 0x02 == TS_program_map_section
					}
				}

				if (section.pid === 0 && !hasProgramAssociationTable) {
					const bitstream = new Bitstream(section.payload);
					const pointerField = bitstream.readAlignedByte();

					bitstream.skipBits(8 * pointerField);

					bitstream.skipBits(14);
					const sectionLength = bitstream.readBits(10);

					bitstream.skipBits(40);

					while (8 * (sectionLength + BYTES_BEFORE_SECTION_LENGTH) - bitstream.pos > BITS_IN_CRC_32) {
						const programNumber = bitstream.readBits(16);
						bitstream.skipBits(3); // Reserved
						const id = bitstream.readBits(13);

						if (programNumber !== 0) {
							if (programMapPid !== null) {
								throw new Error('Only files with a single program are supported.');
							} else {
								programMapPid = id;
							}
						}
					}

					if (programMapPid === null) {
						throw new Error('Program Association Table must link to a Program Map Table.');
					}

					hasProgramAssociationTable = true;
				} else if ((section.pid === programMapPid || isProbablyProgramMap) && !hasProgramMap) {
					const bitstream = new Bitstream(section.payload);
					const pointerField = bitstream.readAlignedByte();

					bitstream.skipBits(8 * pointerField);

					bitstream.skipBits(12);
					const sectionLength = bitstream.readBits(12);

					bitstream.skipBits(43);
					// eslint-disable-next-line @typescript-eslint/no-unused-vars
					const pcrPid = bitstream.readBits(13);

					bitstream.skipBits(6);

					// "The remaining 10 bits specify the number of bytes of the descriptors immediately following the
					// program_info_length field"
					const programInfoLength = bitstream.readBits(10);
					bitstream.skipBits(8 * programInfoLength);

					while (8 * (sectionLength + BYTES_BEFORE_SECTION_LENGTH) - bitstream.pos > BITS_IN_CRC_32) {
						const streamType = bitstream.readBits(8);
						bitstream.skipBits(3);
						const elementaryPid = bitstream.readBits(13);

						bitstream.skipBits(6);
						const esInfoLength = bitstream.readBits(10);

						// Check ES descriptors to detect AC-3/E-AC-3 in System B
						const esInfoEndPos = bitstream.pos + 8 * esInfoLength;
						let hasAc3Descriptor = false;
						let hasEac3Descriptor = false;
						while (bitstream.pos < esInfoEndPos) {
							const descriptorTag = bitstream.readBits(8);
							const descriptorLength = bitstream.readBits(8);
							if (descriptorTag === 0x6a) {
								hasAc3Descriptor = true;
							} else if (descriptorTag === 0x7a || descriptorTag === 0xcc) {
								hasEac3Descriptor = true;
							}
							bitstream.skipBits(8 * descriptorLength);
						}

						let info: ElementaryStream['info'] | null = null;

						switch (streamType) {
							case MpegTsStreamType.AVC:
							case MpegTsStreamType.HEVC: {
								const codec = streamType === MpegTsStreamType.AVC ? 'avc' : 'hevc';

								info = {
									type: 'video',
									codec,
									decoderConfig: null,
									avcCodecInfo: null,
									hevcCodecInfo: null,
									colorSpace: {
										primaries: null,
										transfer: null,
										matrix: null,
										fullRange: null,
									},
									width: -1,
									height: -1,
									squarePixelWidth: -1,
									squarePixelHeight: -1,
									reorderSize: -1,
								};
							}; break;

							case MpegTsStreamType.MP3_MPEG1:
							case MpegTsStreamType.MP3_MPEG2:
							case MpegTsStreamType.AAC:
							case MpegTsStreamType.AC3_SYSTEM_A:
							case MpegTsStreamType.EAC3_SYSTEM_A: {
								let codec: AudioCodec;
								if (
									streamType === MpegTsStreamType.MP3_MPEG1
									|| streamType === MpegTsStreamType.MP3_MPEG2
								) {
									codec = 'mp3';
								} else if (streamType === MpegTsStreamType.AAC) {
									codec = 'aac';
								} else if (streamType === MpegTsStreamType.AC3_SYSTEM_A) {
									codec = 'ac3';
								} else if (streamType === MpegTsStreamType.EAC3_SYSTEM_A) {
									codec = 'eac3';
								} else {
									throw new Error('Unreachable.');
								}

								info = {
									type: 'audio',
									codec,
									decoderConfig: null,
									aacCodecInfo: null,
									numberOfChannels: -1,
									sampleRate: -1,
								};
							}; break;

							case MpegTsStreamType.PRIVATE_DATA: {
								if (hasEac3Descriptor) {
									info = {
										type: 'audio',
										codec: 'eac3',
										decoderConfig: null,
										aacCodecInfo: null,
										numberOfChannels: -1,
										sampleRate: -1,
									};
								} else if (hasAc3Descriptor) {
									info = {
										type: 'audio',
										codec: 'ac3',
										decoderConfig: null,
										aacCodecInfo: null,
										numberOfChannels: -1,
										sampleRate: -1,
									};
								}
							}; break;

							default: {
								// If we don't recognize the codec, we don't surface the track at all. This is because
								// we can't determine its metadata and also have no idea how to packetize its data.

								if (!ignoredStreamTypes.has(streamType)) {
									console.warn(
										`Note: MPEG-TS streams with stream_type 0x${streamType.toString(16)} are not`
										+ ` currently supported.`,
									);
									ignoredStreamTypes.add(streamType);
								}
							}
						}

						if (info) {
							this.elementaryStreams.push({
								demuxer: this,
								pid: elementaryPid,
								streamType,
								initialized: false,
								firstSection: null,
								canBeTrustedWithKeyPackets: false,
								info,
								referencePesPackets: [],
							});
						}
					}

					hasProgramMap = true;
				} else {
					const elementaryStream = this.elementaryStreams.find(x => x.pid === section.pid);
					outer:
					if (elementaryStream && !elementaryStream.initialized) {
						const pesPacket = readPesPacket(section, true);
						if (!pesPacket) {
							throw new Error(
								`Couldn't read first PES packet for Elementary Stream with PID ${elementaryStream.pid}`,
							);
						}

						elementaryStream.firstSection = section;
						elementaryStream.canBeTrustedWithKeyPackets = section.randomAccessIndicator === 1;

						if (this.input._initInput) {
							const initDemuxer = (await this.input._initInput._getDemuxer()) as MpegTsDemuxer;
							const matchingStream = initDemuxer.elementaryStreams.find(x => (
								x.pid === section.pid && x.info.codec === elementaryStream.info.codec
							));

							if (matchingStream) {
								elementaryStream.info = matchingStream.info;
								elementaryStream.initialized = true;

								break outer; // We have the stream info, we're done
							}
						}

						const context = new PacketReadingContext(elementaryStream, pesPacket);

						if (elementaryStream.info.type === 'video') {
							// We loop because in some files, the video parameters are not in the first packet
							while (true) {
								const contextAlias = context; // TyyyyypeScript 😩
								contextAlias.suppliedPacket = null;
								await context.markNextPacket();

								if (elementaryStream.info.codec === 'avc') {
									if (!context.suppliedPacket) {
										throw new Error(
											'Invalid AVC video stream; could not extract AVCDecoderConfigurationRecord'
											+ ' from any packet.',
										);
									}

									elementaryStream.info.avcCodecInfo
										= extractAvcDecoderConfigurationRecord(context.suppliedPacket.data);

									if (!elementaryStream.info.avcCodecInfo) {
										continue; // Search the next packet for it
									}

									const spsUnit = elementaryStream.info.avcCodecInfo.sequenceParameterSets[0];
									assert(spsUnit);
									const spsInfo = parseAvcSps(spsUnit)!;

									elementaryStream.info.width = spsInfo.displayWidth;
									elementaryStream.info.height = spsInfo.displayHeight;

									const num = spsInfo.pixelAspectRatio.num;
									const den = spsInfo.pixelAspectRatio.den;

									if (num > 0 && den > 0) {
										if (num > den) {
											elementaryStream.info.squarePixelWidth = Math.round(
												elementaryStream.info.width * num / den,
											);
											elementaryStream.info.squarePixelHeight = elementaryStream.info.height;
										} else {
											elementaryStream.info.squarePixelWidth = elementaryStream.info.width;
											elementaryStream.info.squarePixelHeight = Math.round(
												elementaryStream.info.height * den / num,
											);
										}
									}

									elementaryStream.info.colorSpace = {
										primaries:
											COLOR_PRIMARIES_MAP_INVERSE[spsInfo.colourPrimaries] as
											VideoColorPrimaries | undefined,
										transfer:
											TRANSFER_CHARACTERISTICS_MAP_INVERSE[spsInfo.transferCharacteristics] as
											VideoTransferCharacteristics | undefined,
										matrix:
											MATRIX_COEFFICIENTS_MAP_INVERSE[spsInfo.matrixCoefficients] as
											VideoMatrixCoefficients | undefined,
										fullRange: !!spsInfo.fullRangeFlag,
									};
									elementaryStream.info.reorderSize = spsInfo.maxDecFrameBuffering;

									break;
								} else if (elementaryStream.info.codec === 'hevc') {
									if (!context.suppliedPacket) {
										throw new Error(
											'Invalid HEVC video stream; could not extract HVCDecoderConfigurationRecord'
											+ ' from first packet.',
										);
									}

									elementaryStream.info.hevcCodecInfo
										= extractHevcDecoderConfigurationRecord(context.suppliedPacket.data);

									if (!elementaryStream.info.hevcCodecInfo) {
										continue; // Search the next packet for it
									}

									const spsArray = elementaryStream.info.hevcCodecInfo.arrays.find(
										a => a.nalUnitType === HevcNalUnitType.SPS_NUT,
									)!;
									const spsUnit = spsArray.nalUnits[0];
									assert(spsUnit);
									const spsInfo = parseHevcSps(spsUnit)!;

									elementaryStream.info.width = spsInfo.displayWidth;
									elementaryStream.info.height = spsInfo.displayHeight;
									if (spsInfo.pixelAspectRatio.num > spsInfo.pixelAspectRatio.den) {
										elementaryStream.info.squarePixelWidth = Math.round(
											elementaryStream.info.width
											* spsInfo.pixelAspectRatio.num / spsInfo.pixelAspectRatio.den,
										);
										elementaryStream.info.squarePixelHeight = elementaryStream.info.height;
									} else {
										elementaryStream.info.squarePixelWidth = elementaryStream.info.width;
										elementaryStream.info.squarePixelHeight = Math.round(
											elementaryStream.info.height
											* spsInfo.pixelAspectRatio.den / spsInfo.pixelAspectRatio.num,
										);
									}

									elementaryStream.info.colorSpace = {
										primaries:
											COLOR_PRIMARIES_MAP_INVERSE[spsInfo.colourPrimaries] as
											VideoColorPrimaries | undefined,
										transfer:
											TRANSFER_CHARACTERISTICS_MAP_INVERSE[spsInfo.transferCharacteristics] as
											VideoTransferCharacteristics | undefined,
										matrix:
											MATRIX_COEFFICIENTS_MAP_INVERSE[spsInfo.matrixCoefficients] as
											VideoMatrixCoefficients | undefined,
										fullRange: !!spsInfo.fullRangeFlag,
									};
									elementaryStream.info.reorderSize = spsInfo.maxDecFrameBuffering;

									break;
								} else {
									throw new Error('Unhandled.');
								}
							}

							elementaryStream.info.decoderConfig = {
								codec: extractVideoCodecString({
									width: elementaryStream.info.width,
									height: elementaryStream.info.height,
									codec: elementaryStream.info.codec,
									codecDescription: null,
									colorSpace: elementaryStream.info.colorSpace,
									avcType: 1,
									avcCodecInfo: elementaryStream.info.avcCodecInfo,
									hevcCodecInfo: elementaryStream.info.hevcCodecInfo,
									vp9CodecInfo: null,
									av1CodecInfo: null,
								}),
								codedWidth: elementaryStream.info.width,
								codedHeight: elementaryStream.info.height,
								colorSpace: elementaryStream.info.colorSpace,
							};

							if (
								elementaryStream.info.width !== elementaryStream.info.squarePixelWidth
								|| elementaryStream.info.height !== elementaryStream.info.squarePixelHeight
							) {
								elementaryStream.info.decoderConfig.displayAspectWidth
									= elementaryStream.info.squarePixelWidth;
								elementaryStream.info.decoderConfig.displayAspectHeight
									= elementaryStream.info.squarePixelHeight;
							}

							elementaryStream.initialized = true;
						} else {
							await context.markNextPacket();
							if (!context.suppliedPacket) {
								throw new Error(
									`Couldn't parse first media packet for Elementary Stream with`
									+ ` PID ${elementaryStream.pid}`,
								);
							}

							if (elementaryStream.info.codec === 'aac') {
								const slice = FileSlice.tempFromBytes(context.suppliedPacket.data);
								const header = readAdtsFrameHeader(slice);
								if (!header) {
									throw new Error(
										'Invalid AAC audio stream; could not read ADTS frame header from first packet.',
									);
								}

								elementaryStream.info.aacCodecInfo = {
									isMpeg2: false,
									objectType: header.objectType,
								};
								elementaryStream.info.numberOfChannels
									= aacChannelMap[header.channelConfiguration]!;
								elementaryStream.info.sampleRate
									= aacFrequencyTable[header.samplingFrequencyIndex]!;
							} else if (elementaryStream.info.codec === 'mp3') {
								const word = readU32Be(FileSlice.tempFromBytes(context.suppliedPacket.data));
								const result = readMp3FrameHeader(word, context.suppliedPacket.data.byteLength);
								if (!result.header) {
									throw new Error(
										'Invalid MP3 audio stream; could not read frame header from first packet.',
									);
								}

								elementaryStream.info.numberOfChannels = getMp3ChannelCount(result.header.channel);
								elementaryStream.info.sampleRate = result.header.sampleRate;
							} else if (elementaryStream.info.codec === 'ac3') {
								const frameInfo = parseAc3SyncFrame(context.suppliedPacket.data);
								if (!frameInfo) {
									throw new Error(
										'Invalid AC-3 audio stream; could not read sync frame from first packet.',
									);
								}

								if (frameInfo.fscod === 3) {
									throw new Error(
										'Invalid AC-3 audio stream; reserved sample rate code found in first packet.',
									);
								}

								elementaryStream.info.numberOfChannels
									= AC3_ACMOD_CHANNEL_COUNTS[frameInfo.acmod]! + frameInfo.lfeon;
								elementaryStream.info.sampleRate = AC3_SAMPLE_RATES[frameInfo.fscod]!;
							} else if (elementaryStream.info.codec === 'eac3') {
								const frameInfo = parseEac3SyncFrame(context.suppliedPacket.data);
								if (!frameInfo) {
									throw new Error(
										'Invalid E-AC-3 audio stream; could not read sync frame from first packet.',
									);
								}

								const sampleRate = getEac3SampleRate(frameInfo);
								if (sampleRate === null) {
									throw new Error(
										'Invalid E-AC-3 audio stream; reserved sample rate code found in first packet.',
									);
								}

								elementaryStream.info.numberOfChannels = getEac3ChannelCount(frameInfo);
								elementaryStream.info.sampleRate = sampleRate;
							} else {
								throw new Error('Unhandled.');
							}

							elementaryStream.info.decoderConfig = {
								codec: extractAudioCodecString({
									codec: elementaryStream.info.codec,
									codecDescription: null,
									aacCodecInfo: elementaryStream.info.aacCodecInfo,
								}),
								numberOfChannels: elementaryStream.info.numberOfChannels,
								sampleRate: elementaryStream.info.sampleRate,
							};
							elementaryStream.initialized = true;
						}
					}
				}

				const isDone = hasProgramMap && this.elementaryStreams.every(x => x.initialized);
				if (isDone) {
					break;
				}

				currentPos += this.packetStride;
			}

			if (!hasProgramMap) {
				if (!hasProgramAssociationTable) {
					throw new Error('No Program Association Table found in the file.');
				}

				throw new Error('No Program Map Table found in the file.');
			}

			for (const stream of this.elementaryStreams) {
				if (stream.info.type === 'video') {
					this.trackBackingEntries.push(
						new MpegTsVideoTrackBacking(stream as ElementaryVideoStream),
					);
				} else {
					this.trackBackingEntries.push(
						new MpegTsAudioTrackBacking(stream as ElementaryAudioStream),
					);
				}
			}
		})();
	}

	async getTrackBackings() {
		await this.readMetadata();
		return this.trackBackingEntries;
	}

	async getMetadataTags(): Promise<MetadataTags> {
		return {}; // Nothing for now
	}

	async getMimeType(): Promise<string> {
		await this.readMetadata();

		const codecStrings = await Promise.all(this.trackBackingEntries.map(
			x => x.getDecoderConfig().then(c => c?.codec ?? null),
		));

		return buildMpegTsMimeType(codecStrings);
	}

	async readSection(startPos: number, full: boolean, contiguous = false): Promise<Section | null> {
		let endPos = startPos;
		let currentPos = startPos;
		const chunks: Uint8Array[] = [];
		let chunksByteLength = 0;
		let firstPacket: TsPacket | null = null;
		let mustAddSectionEnd = true;
		let randomAccessIndicator = 0;

		while (true) {
			const packet = await this.readPacket(currentPos);
			currentPos += this.packetStride;

			if (!packet) {
				break;
			}

			if (!firstPacket) {
				if (packet.payloadUnitStartIndicator === 0) {
					break;
				}

				firstPacket = packet;
			} else {
				if (packet.pid !== firstPacket.pid) {
					if (contiguous) {
						break; // End of section
					} else {
						continue; // Ignore this packet
					}
				}

				if (packet.payloadUnitStartIndicator === 1) {
					break;
				}
			}

			const hasAdaptationField = !!(packet.adaptationFieldControl & 0b10);
			const hasPayload = !!(packet.adaptationFieldControl & 0b01);

			let adaptationFieldLength = 0;
			if (hasAdaptationField) {
				adaptationFieldLength = 1 + packet.body[0]!;

				// Extract random_access_indicator from first packet's adaptation field
				if (packet === firstPacket && adaptationFieldLength > 1) {
					randomAccessIndicator = (packet.body[1]! >> 6) & 1;
				}
			}

			if (hasPayload) {
				if (adaptationFieldLength === 0) {
					chunks.push(packet.body);
					chunksByteLength += packet.body.byteLength;
				} else {
					chunks.push(packet.body.subarray(adaptationFieldLength));
					chunksByteLength += packet.body.byteLength - adaptationFieldLength;
				}
			}

			endPos = currentPos;

			// 64 is just "a bit of data", enough for the PES packet header
			if (!full && chunksByteLength >= 64) {
				mustAddSectionEnd = false; // Not the actual section end
				break;
			}

			// Check if we already know this is a section end
			const isKnownSectionEnd = binarySearchExact(this.sectionEndPositions, endPos, x => x) !== -1;
			if (isKnownSectionEnd) {
				mustAddSectionEnd = false;
				break;
			}
		}

		if (mustAddSectionEnd) {
			const index = binarySearchLessOrEqual(this.sectionEndPositions, endPos, x => x);
			this.sectionEndPositions.splice(index + 1, 0, endPos);
		}

		if (!firstPacket) {
			return null;
		}

		let merged: Uint8Array;
		if (chunks.length === 1) {
			merged = chunks[0]!;
		} else {
			const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
			merged = new Uint8Array(totalLength);
			let offset = 0;
			for (const chunk of chunks) {
				merged.set(chunk, offset);
				offset += chunk.length;
			}
		}

		return {
			startPos,
			endPos: full ? endPos : null,
			pid: firstPacket.pid,
			payload: merged,
			randomAccessIndicator,
		};
	}

	async readPacketHeader(pos: number): Promise<TsPacketHeader | null> {
		let slice = this.reader.requestSlice(pos, 4);
		if (slice instanceof Promise) slice = await slice;

		if (!slice) {
			return null;
		}

		const syncByte = readU8(slice);
		if (syncByte !== 0x47) {
			throw new Error('Invalid TS packet sync byte. Likely an internal bug, please report this file.');
		}

		const nextTwoBytes = readU16Be(slice);
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		const transportErrorIndicator = nextTwoBytes >> 15;
		const payloadUnitStartIndicator = (nextTwoBytes >> 14) & 0x1;
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		const transportPriority = (nextTwoBytes >> 13) & 0x1;
		const pid = nextTwoBytes & 0x1FFF;

		const nextByte = readU8(slice);
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		const transportScramblingControl = nextByte >> 6;
		const adaptationFieldControl = (nextByte >> 4) & 0x3;
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		const continuityCounter = nextByte & 0xF;

		return {
			payloadUnitStartIndicator,
			pid,
			adaptationFieldControl,
		};
	}

	async readPacket(pos: number): Promise<TsPacket | null> {
		// Code in here is duplicated from readPacketHeader for performance reasons
		let slice = this.reader.requestSlice(pos, TS_PACKET_SIZE);
		if (slice instanceof Promise) slice = await slice;

		if (!slice) {
			return null;
		}

		const bytes = readBytes(slice, TS_PACKET_SIZE);

		const syncByte = bytes[0]!;
		if (syncByte !== 0x47) {
			throw new Error('Invalid TS packet sync byte. Likely an internal bug, please report this file.');
		}

		const nextTwoBytes = (bytes[1]! << 8) + bytes[2]!;
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		const transportErrorIndicator = nextTwoBytes >> 15;
		const payloadUnitStartIndicator = (nextTwoBytes >> 14) & 0x1;
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		const transportPriority = (nextTwoBytes >> 13) & 0x1;
		const pid = nextTwoBytes & 0x1FFF;

		const nextByte = bytes[3]!;
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		const transportScramblingControl = nextByte >> 6;
		const adaptationFieldControl = (nextByte >> 4) & 0x3;
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		const continuityCounter = nextByte & 0xF;

		return {
			payloadUnitStartIndicator,
			pid,
			adaptationFieldControl,
			body: bytes.subarray(4),
		};
	}
}

type PesPacketHeader = {
	sectionStartPos: number;
	sectionEndPos: number | null; // null if the section wasn't read fully
	pts: number | null;
	randomAccessIndicator: number;
};

type TimestampedPesPacketHeader = PesPacketHeader & {
	pts: number;
};

type PesPacket = PesPacketHeader & {
	data: Uint8Array<ArrayBufferLike>;
};

type TimestampedPesPacket = PesPacket & {
	pts: number;
};

const readPesPacketHeader = <T extends boolean>(
	section: Section,
	expectPts: T,
): (T extends true ? TimestampedPesPacketHeader : PesPacketHeader) | null => {
	if (section.payload.byteLength < 3) {
		return null;
	}

	const bitstream = new Bitstream(section.payload);

	const startCodePrefix = bitstream.readBits(24);
	if (startCodePrefix !== 0x000001) {
		return null;
	}

	const streamId = bitstream.readBits(8);
	bitstream.skipBits(16);

	if (
		streamId === 0b10111100 // program_stream_map
		|| streamId === 0b10111110 // padding_stream
		|| streamId === 0b10111111 // private_stream_2
		|| streamId === 0b11110000 // ECM
		|| streamId === 0b11110001 // EMM
		|| streamId === 0b11111111 // program_stream_directory
		|| streamId === 0b11110010 // DSMCC_stream
		|| streamId === 0b11111000 // ITU-T Rec. H.222.1 type E stream
	) {
		return null;
	}

	bitstream.skipBits(8);

	const ptsDtsFlags = bitstream.readBits(2);

	bitstream.skipBits(14);

	let pts: number | null = null;
	if (ptsDtsFlags === 0b10 || ptsDtsFlags === 0b11) {
		pts = 0;

		bitstream.skipBits(4);
		pts += bitstream.readBits(3) * (1 << 30);
		bitstream.skipBits(1);
		pts += bitstream.readBits(15) * (1 << 15);
		bitstream.skipBits(1);
		pts += bitstream.readBits(15);
	} else {
		if (expectPts) {
			throw new Error(MISSING_PTS_ERROR_MESSAGE);
		}
	}

	return {
		sectionStartPos: section.startPos,
		sectionEndPos: section.endPos,
		pts,
		randomAccessIndicator: section.randomAccessIndicator,
	} as T extends true ? TimestampedPesPacketHeader : PesPacketHeader;
};

const readPesPacket = <T extends boolean>(
	section: Section,
	expectPts: T,
): (T extends true ? TimestampedPesPacket : PesPacket) | null => {
	assert(section.endPos !== null); // Can only read full PES packets from fully read sections

	const header = readPesPacketHeader(section, expectPts);
	if (!header) {
		return null;
	}

	const bitstream = new Bitstream(section.payload);
	bitstream.skipBits(32);
	const pesPacketLength = bitstream.readBits(16);
	const BYTES_UNTIL_END_OF_PES_PACKET_LENGTH = 6;

	bitstream.skipBits(16);
	const pesHeaderDataLength = bitstream.readBits(8);
	const pesHeaderEndPos = bitstream.pos + 8 * pesHeaderDataLength;

	bitstream.pos = pesHeaderEndPos;

	const bytePos = pesHeaderEndPos / 8;
	assert(Number.isInteger(bytePos));

	const data = section.payload.subarray(
		bytePos,
		// "A value of 0 indicates that the PES packet length is neither specified nor bounded and is allowed only in
		// PES packets whose payload consists of bytes from a video elementary stream contained in
		// transport stream packets."
		pesPacketLength > 0
			? BYTES_UNTIL_END_OF_PES_PACKET_LENGTH + pesPacketLength
			: section.payload.byteLength,
	);

	return {
		...header,
		data,
	} as T extends true ? TimestampedPesPacket : PesPacket;
};

abstract class MpegTsTrackBacking implements InputTrackBacking {
	packetBuffers = new WeakMap<EncodedPacket, PacketBuffer>();
	/** Used for recreating PacketBuffers if necessary. */
	packetSectionStarts = new WeakMap<EncodedPacket, number>();

	constructor(public elementaryStream: ElementaryStream) {}

	abstract getType(): TrackType;
	abstract getDecoderConfig(): Promise<VideoDecoderConfig | AudioDecoderConfig | null>;

	getId() {
		return this.elementaryStream.pid;
	}

	getNumber() {
		const demuxer = this.elementaryStream.demuxer;
		const trackType = this.elementaryStream.info.type;

		let number = 0;
		for (const backing of demuxer.trackBackingEntries) {
			if (backing.getType() === trackType) {
				number++;
			}

			assert(backing instanceof MpegTsTrackBacking);
			if (backing.elementaryStream === this.elementaryStream) {
				break;
			}
		}

		return number;
	}

	getCodec(): MediaCodec | null {
		throw new Error('Not implemented on base class.');
	}

	getInternalCodecId() {
		return this.elementaryStream.streamType;
	}

	getName() {
		return null;
	}

	getLanguageCode() {
		return UNDETERMINED_LANGUAGE;
	}

	getDisposition(): TrackDisposition {
		return {
			...DEFAULT_TRACK_DISPOSITION,
			primary: false,
		};
	}

	getTimeResolution() {
		return TIMESCALE;
	}

	isRelativeToUnixEpoch() {
		return false;
	}

	getPairingMask() {
		return 1n;
	}

	getBitrate() {
		return null;
	}

	getAverageBitrate() {
		return null;
	}

	async getDurationFromMetadata() {
		return null;
	}

	async getLiveRefreshInterval() {
		return null;
	}

	abstract allPacketsAreKeyPackets(): boolean;
	abstract getReorderSize(): number;

	createEncodedPacket(
		suppliedPacket: SuppliedPacket,
		duration: number,
		options: PacketRetrievalOptions,
	) {
		let packetType: PacketType;

		if (this.allPacketsAreKeyPackets()) {
			packetType = 'key';
		} else {
			packetType = suppliedPacket.randomAccessIndicator === 1
				? 'key'
				: 'delta';
		}

		return new EncodedPacket(
			options.metadataOnly ? PLACEHOLDER_DATA : suppliedPacket.data,
			packetType,
			suppliedPacket.pts / TIMESCALE,
			Math.max(duration / TIMESCALE, 0),
			suppliedPacket.sequenceNumber,
			suppliedPacket.data.byteLength,
		);
	}

	async getFirstPacket(options: PacketRetrievalOptions): Promise<EncodedPacket | null> {
		const section = this.elementaryStream.firstSection;
		assert(section);

		const pesPacket = readPesPacket(section, true);
		assert(pesPacket);

		const context = new PacketReadingContext(this.elementaryStream, pesPacket);
		const buffer = new PacketBuffer(this, context);

		const result = await buffer.readNext();
		if (!result) {
			return null;
		}

		const packet = this.createEncodedPacket(result.packet, result.duration, options);
		this.packetBuffers.set(packet, buffer);
		this.packetSectionStarts.set(packet, result.packet.sectionStartPos);

		return packet;
	}

	async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise<EncodedPacket | null> {
		let buffer = this.packetBuffers.get(packet);

		if (buffer) {
			// Fast path
			const result = await buffer.readNext();
			if (!result) {
				return null;
			}

			// Remove PacketBuffer access from the old packet, it belongs to the next packet now
			this.packetBuffers.delete(packet);

			const newPacket = this.createEncodedPacket(result.packet, result.duration, options);
			this.packetBuffers.set(newPacket, buffer);
			this.packetSectionStarts.set(newPacket, result.packet.sectionStartPos);

			return newPacket;
		}

		// No buffer, we gotta do some rereading
		const sectionStartPos = this.packetSectionStarts.get(packet);
		if (sectionStartPos === undefined) {
			throw new Error('Packet was not created from this track.');
		}

		const demuxer = this.elementaryStream.demuxer;
		const section = await demuxer.readSection(sectionStartPos, true);
		assert(section);

		const pesPacket = readPesPacket(section, true);
		assert(pesPacket);

		const context = new PacketReadingContext(this.elementaryStream, pesPacket);
		buffer = new PacketBuffer(this, context);

		// Advance until we pass the current packet's sequence number
		const targetSequenceNumber = packet.sequenceNumber;
		while (true) {
			const result = await buffer.readNext();
			if (!result) {
				return null;
			}

			if (result.packet.sequenceNumber > targetSequenceNumber) {
				// We found the next packet!
				const newPacket = this.createEncodedPacket(result.packet, result.duration, options);
				this.packetBuffers.set(newPacket, buffer);
				this.packetSectionStarts.set(newPacket, result.packet.sectionStartPos);
				return newPacket;
			}
		}
	}

	async getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise<EncodedPacket | null> {
		let currentPacket: EncodedPacket | null = packet;

		// Just loop until we hit one
		while (true) {
			currentPacket = await this.getNextPacket(currentPacket, options);

			if (!currentPacket) {
				return null;
			}

			if (currentPacket.type === 'key') {
				return currentPacket;
			}
		}
	}

	getPacket(timestamp: number, options: PacketRetrievalOptions): Promise<EncodedPacket | null> {
		return this.doPacketLookup(timestamp, false, options);
	}

	getKeyPacket(timestamp: number, options: PacketRetrievalOptions): Promise<EncodedPacket | null> {
		return this.doPacketLookup(timestamp, true, options);
	}

	/**
	 * Searches for the packet with the largest timestamp not larger than `timestamp` in the file, using a combination
	 * of chunk-based binary search and linear refinement. The reason the coarse search is done in large chunks is to
	 * make it more performant for small files and over high-latency readers such as the network.
	 */
	async doPacketLookup(
		timestamp: number,
		keyframesOnly: boolean,
		options: PacketRetrievalOptions,
	): Promise<EncodedPacket | null> {
		const searchPts = roundIfAlmostInteger(timestamp * TIMESCALE);

		const demuxer = this.elementaryStream.demuxer;
		const { reader, seekChunkSize } = demuxer;
		const pid = this.elementaryStream.pid;

		const findFirstPesPacketHeaderInChunk = async (
			startPos: number,
			endPos: number,
			readSectionInFull: boolean,
		) => {
			let currentPos = startPos;

			while (currentPos < endPos) {
				const packetHeader = await demuxer.readPacketHeader(currentPos);
				if (!packetHeader) {
					return null;
				}

				if (packetHeader.pid === pid && packetHeader.payloadUnitStartIndicator === 1) {
					const section = await demuxer.readSection(currentPos, readSectionInFull);
					if (!section) {
						return null;
					}

					const pesPacketHeader = readPesPacketHeader(section, false);
					if (pesPacketHeader && pesPacketHeader.pts !== null) {
						return {
							pesPacketHeader: pesPacketHeader as TimestampedPesPacketHeader,
							section,
						};
					}
				}

				currentPos += demuxer.packetStride;
			}

			return null;
		};

		// Get the first PES packet of the track
		const firstSection = this.elementaryStream.firstSection;
		assert(firstSection);
		const firstPesPacketHeader = readPesPacketHeader(firstSection, true);
		assert(firstPesPacketHeader);

		if (searchPts < firstPesPacketHeader.pts) {
			// We're before the first packet, definitely nothing here
			return null;
		}

		let scanStartPos: number;

		const referencePesPackets = this.elementaryStream.referencePesPackets;
		const referencePointIndex = binarySearchLessOrEqual(referencePesPackets, searchPts, x => x.pts);
		const referencePoint = referencePointIndex !== -1 ? referencePesPackets[referencePointIndex]! : null;
		if (referencePoint && searchPts - referencePoint.pts < TIMESCALE / 2) {
			// Reference point ain't too far away, prefer it over the chunk search
			scanStartPos = referencePoint.sectionStartPos;
		} else {
			let startChunkIndex = 0;

			if (reader.fileSize !== null) {
				const numChunks = Math.ceil(reader.fileSize / seekChunkSize);

				if (numChunks > 1) {
					// Binary search to find the chunk with highest index whose first PES has pts <= searchPts
					let low = 0;
					let high = numChunks - 1;
					startChunkIndex = low;

					while (low <= high) {
						const mid = Math.floor((low + high) / 2);
						const chunkStartPos = floorToMultiple(mid * seekChunkSize, demuxer.packetStride)
							+ firstPesPacketHeader.sectionStartPos;
						const chunkEndPos = chunkStartPos + seekChunkSize;

						const result = await findFirstPesPacketHeaderInChunk(chunkStartPos, chunkEndPos, false);

						if (!result) {
							// No PES packet found in this chunk, search left
							high = mid - 1;
							continue;
						}

						if (result.pesPacketHeader.pts <= searchPts) {
							// This chunk's first PES is <= searchPts, it's a candidate
							startChunkIndex = mid;
							low = mid + 1; // Search right
						} else {
							// Search left
							high = mid - 1;
						}
					}
				}
			}

			scanStartPos = floorToMultiple(
				startChunkIndex * seekChunkSize,
				demuxer.packetStride,
			) + firstPesPacketHeader.sectionStartPos;
		}

		// Find the first PES packet at or after scanStartPos
		const result = await findFirstPesPacketHeaderInChunk(
			scanStartPos,
			reader.fileSize ?? Infinity,
			false,
		);

		let currentPesHeader = result?.pesPacketHeader ?? null;
		if (!currentPesHeader) {
			// Fall back to first packet
			currentPesHeader = firstPesPacketHeader;
		}

		const reorderSize = this.getReorderSize();

		const retrieveEncodedPacket = async (
			sectionStartPos: number,
			predicate: (packet: SuppliedPacket) => boolean,
		) => {
			// Load the relevant section in full
			const section = await demuxer.readSection(sectionStartPos, true);
			assert(section);

			const pesPacket = readPesPacket(section, true);
			assert(pesPacket);

			const context = new PacketReadingContext(this.elementaryStream, pesPacket);
			const buffer = new PacketBuffer(this, context);

			// Advance until the top-most presentation timestamp crosses or equals searchPts
			while (true) {
				const topPts = last(buffer.presentationOrderPackets)?.pts ?? -Infinity;
				if (topPts >= searchPts) {
					break;
				}

				const didRead = await buffer.readNextPacket();
				if (!didRead) {
					break;
				}
			}

			const targetIndex = findLastIndex(buffer.presentationOrderPackets, predicate);
			if (targetIndex === -1) {
				return null;
			}

			const targetPacket = buffer.presentationOrderPackets[targetIndex]!;
			const lastDuration = targetIndex === 0
				? 0
				: targetPacket.pts - buffer.presentationOrderPackets[targetIndex - 1]!.pts;

			// Pop packets in decode order until we hit the target packet
			while (buffer.decodeOrderPackets[0] !== targetPacket) {
				buffer.decodeOrderPackets.shift();
			}
			buffer.lastDuration = lastDuration; // Kinda ugly but necessary fix

			const result = await buffer.readNext();
			assert(result);

			const packet = this.createEncodedPacket(result.packet, result.duration, options);
			this.packetBuffers.set(packet, buffer);
			this.packetSectionStarts.set(packet, result.packet.sectionStartPos);

			return packet;
		};

		if (!keyframesOnly || this.allPacketsAreKeyPackets()) {
			// Normat packet lookup case. Slightly easier since we just need to search (mostly) forward to find the
			// packet.

			// Linear scan to find the PES packet with largest pts <= searchPts. This will be used as the "midpoint"
			// of the next refinement step (which is needed because of B-frames).
			outer:
			while (true) {
				let currentPos = currentPesHeader.sectionStartPos + demuxer.packetStride;

				while (true) {
					const packetHeader = await demuxer.readPacketHeader(currentPos);
					if (!packetHeader) {
						break outer; // End of file
					}

					if (packetHeader.pid === pid && packetHeader.payloadUnitStartIndicator === 1) {
						const section = await demuxer.readSection(currentPos, false);
						if (section) {
							const nextPesHeader = readPesPacketHeader(section, false);
							if (nextPesHeader && nextPesHeader.pts !== null) {
								if (nextPesHeader.pts > searchPts) {
									break outer;
								}

								currentPesHeader = nextPesHeader as TimestampedPesPacketHeader;
								maybeInsertReferencePacket(this.elementaryStream, currentPesHeader);

								break;
							}
						}
					}

					currentPos += demuxer.packetStride;
				}
			}

			// Rewind by reorderSize + 1 PES packets (even for audio! To ensure proper durations)
			outer:
			for (let i = 0; i < reorderSize + 1; i++) {
				let pos = currentPesHeader.sectionStartPos - demuxer.packetStride;

				while (pos >= demuxer.packetOffset) {
					const packetHeader = await demuxer.readPacketHeader(pos);
					if (!packetHeader) {
						break outer;
					}

					if (packetHeader.pid === pid && packetHeader.payloadUnitStartIndicator === 1) {
						const section = await demuxer.readSection(pos, false);
						if (section) {
							const header = readPesPacketHeader(section, false);
							if (header && header.pts !== null) {
								currentPesHeader = header as TimestampedPesPacketHeader;
								break;
							}
						}
					}

					pos -= demuxer.packetStride;
				}
			}

			return retrieveEncodedPacket(currentPesHeader.sectionStartPos, p => p.pts <= searchPts);
		} else {
			// Key packet lookup case. Slightly harder since the starting chunk may not have a key packet at all, which
			// means we might need to search the previous chunks until we find something.

			let currentChunkStartPos = scanStartPos;
			let nextChunkStartPos: number | null = null; // "next" as in later in the file, even tho we scan backwards

			const readSectionsInFull = !this.elementaryStream.canBeTrustedWithKeyPackets;

			while (true) {
				let bestKeyPesHeader: TimestampedPesPacketHeader | null = null;

				const isFirstChunk = currentChunkStartPos <= firstPesPacketHeader.sectionStartPos;

				let pesHeader: TimestampedPesPacketHeader | null;
				let pesHeaderSection: Section | null = null;

				if (isFirstChunk) {
					pesHeader = firstPesPacketHeader;
					pesHeaderSection = firstSection;
				} else {
					const result = await findFirstPesPacketHeaderInChunk(
						currentChunkStartPos,
						reader.fileSize ?? Infinity,
						readSectionsInFull,
					);

					pesHeader = result?.pesPacketHeader ?? null;
					pesHeaderSection = result?.section ?? null;
				}

				let passedSearchPts = false;
				let lookaheadCount = 0;

				outer:
				while (pesHeader) {
					if (nextChunkStartPos !== null && pesHeader.sectionStartPos >= nextChunkStartPos) {
						// Stop at the next chunk boundary
						break;
					}

					if (pesHeader.pts <= searchPts) {
						let isKeyPacket: boolean;
						if (this.elementaryStream.canBeTrustedWithKeyPackets) {
							isKeyPacket = pesHeader.randomAccessIndicator === 1;
						} else {
							assert(pesHeaderSection);
							const pesPacket = readPesPacket(pesHeaderSection, true);
							assert(pesPacket);

							const context = new PacketReadingContext(this.elementaryStream, pesPacket);
							await context.markNextPacket();

							isKeyPacket = context.suppliedPacket?.randomAccessIndicator === 1;
						}

						if (isKeyPacket) {
							bestKeyPesHeader = pesHeader;
						}
					}

					if (pesHeader.pts > searchPts) {
						passedSearchPts = true;
					}

					// If we've passed searchPts, do lookahead for reorderSize more packets just to be sure
					if (passedSearchPts) {
						lookaheadCount++;

						if (lookaheadCount > reorderSize) {
							break;
						}
					}

					// Find next PES packet
					let currentPos = pesHeader.sectionStartPos + demuxer.packetStride;

					while (true) {
						const packetHeader = await demuxer.readPacketHeader(currentPos);
						if (!packetHeader) {
							break outer; // End of file
						}

						if (packetHeader.pid === pid && packetHeader.payloadUnitStartIndicator === 1) {
							const section = await demuxer.readSection(currentPos, readSectionsInFull);
							if (section) {
								const nextPesHeader = readPesPacketHeader(section, false);

								if (nextPesHeader && nextPesHeader.pts !== null) {
									pesHeader = nextPesHeader as TimestampedPesPacketHeader;
									pesHeaderSection = section;
									maybeInsertReferencePacket(this.elementaryStream, pesHeader);

									break;
								}
							}
						}

						currentPos += demuxer.packetStride;
					}
				}

				if (bestKeyPesHeader) {
					let startPesHeader = bestKeyPesHeader;

					if (lookaheadCount === 0) {
						// Packet is at the end of stream, let's rewind a little to obtain the correct packet duration
						outer:
						for (let i = 0; i < reorderSize; i++) {
							let pos = startPesHeader.sectionStartPos - demuxer.packetStride;

							while (pos >= demuxer.packetOffset) {
								const packetHeader = await demuxer.readPacketHeader(pos);
								if (!packetHeader) {
									break outer;
								}

								if (packetHeader.pid === pid && packetHeader.payloadUnitStartIndicator === 1) {
									const section = await demuxer.readSection(pos, readSectionsInFull);
									if (section) {
										const header = readPesPacketHeader(section, false);
										if (header && header.pts !== null) {
											startPesHeader = header as TimestampedPesPacketHeader;
											break;
										}
									}
								}

								pos -= demuxer.packetStride;
							}
						}
					}

					const encodedPacket = await retrieveEncodedPacket(
						startPesHeader.sectionStartPos,
						p => p.pts <= searchPts && p.randomAccessIndicator === 1,
					);
					assert(encodedPacket); // There must be one

					return encodedPacket;
				}

				if (isFirstChunk) {
					return null;
				}

				// No key frame found in this chunk, move one chunk to the left
				nextChunkStartPos = currentChunkStartPos;
				currentChunkStartPos = Math.max(
					floorToMultiple(
						currentChunkStartPos - firstPesPacketHeader.sectionStartPos - seekChunkSize,
						demuxer.packetStride,
					) + firstPesPacketHeader.sectionStartPos,
					firstPesPacketHeader.sectionStartPos,
				);
			}
		}
	}
}

class MpegTsVideoTrackBacking extends MpegTsTrackBacking implements InputVideoTrackBacking {
	override elementaryStream!: ElementaryVideoStream;

	getType() {
		return 'video' as const;
	}

	override getCodec(): VideoCodec {
		return this.elementaryStream.info.codec;
	}

	getCodedWidth() {
		return this.elementaryStream.info.width;
	}

	getCodedHeight() {
		return this.elementaryStream.info.height;
	}

	getSquarePixelWidth() {
		return this.elementaryStream.info.squarePixelWidth;
	}

	getSquarePixelHeight() {
		return this.elementaryStream.info.squarePixelHeight;
	}

	getRotation(): Rotation {
		return 0;
	}

	async getColorSpace(): Promise<VideoColorSpaceInit> {
		return this.elementaryStream.info.colorSpace;
	}

	async canBeTransparent() {
		return false;
	}

	async getDecoderConfig(): Promise<VideoDecoderConfig> {
		assert(this.elementaryStream.info.decoderConfig);
		return this.elementaryStream.info.decoderConfig;
	}

	override allPacketsAreKeyPackets(): boolean {
		return false;
	}

	override getReorderSize(): number {
		return this.elementaryStream.info.reorderSize;
	}
}

class MpegTsAudioTrackBacking extends MpegTsTrackBacking implements InputAudioTrackBacking {
	override elementaryStream!: ElementaryAudioStream;

	getType() {
		return 'audio' as const;
	}

	override getCodec(): AudioCodec {
		return this.elementaryStream.info.codec;
	}

	getNumberOfChannels() {
		return this.elementaryStream.info.numberOfChannels;
	}

	getSampleRate() {
		return this.elementaryStream.info.sampleRate;
	}

	async getDecoderConfig(): Promise<AudioDecoderConfig> {
		assert(this.elementaryStream.info.decoderConfig);
		return this.elementaryStream.info.decoderConfig;
	}

	override allPacketsAreKeyPackets(): boolean {
		return true;
	}

	override getReorderSize(): number {
		return 0; // No reordering, since no B-frames because goated
	}
}

const maybeInsertReferencePacket = (
	elementaryStream: ElementaryStream,
	pesPacketHeader: TimestampedPesPacketHeader,
) => {
	const referencePesPackets = elementaryStream.referencePesPackets;

	const index = binarySearchLessOrEqual(
		referencePesPackets,
		pesPacketHeader.sectionStartPos,
		x => x.sectionStartPos,
	);
	if (index >= 0) {
		// Since pts and file position don't necessarily have a monotonic relationship (since pts can go crazy),
		// let's see if inserting at the given index would violate the pts order. If so, return.
		const entry = referencePesPackets[index]!;
		if (pesPacketHeader.pts <= entry.pts) {
			return false;
		}

		const minByteDistance = elementaryStream.demuxer.minReferencePointByteDistance;

		if (pesPacketHeader.sectionStartPos - entry.sectionStartPos < minByteDistance) {
			// Too close
			return false;
		}

		if (index < referencePesPackets.length - 1) {
			const nextEntry = referencePesPackets[index + 1]!;
			if (nextEntry.pts < pesPacketHeader.pts) {
				// Out of order
				return false;
			}

			if (nextEntry.sectionStartPos - pesPacketHeader.sectionStartPos < minByteDistance) {
				// Too close
				return false;
			}
		}
	}

	referencePesPackets.splice(index + 1, 0, pesPacketHeader);
	return true;
};

type SuppliedPacket = {
	pts: number;
	data: Uint8Array;
	sequenceNumber: number;
	sectionStartPos: number;
	randomAccessIndicator: number;
};

/** Stateful context used to extract exact encoded packets from the underlying data stream. */
class PacketReadingContext {
	elementaryStream: ElementaryStream;
	pid: number;
	demuxer: MpegTsDemuxer;
	startingPesPacket: TimestampedPesPacket;

	currentPos = 0; // Relative to the data in startingPesPacket
	pesPackets: PesPacket[] = [];
	currentPesPacketIndex = 0;
	currentPesPacketPos = 0;
	endPos = 0;
	lastSuppliedPesPacket: PesPacket | null = null;
	nextPts: number | null = null;

	suppliedPacket: SuppliedPacket | null = null;

	constructor(elementaryStream: ElementaryStream, startingPesPacket: TimestampedPesPacket) {
		this.elementaryStream = elementaryStream;
		this.pid = elementaryStream.pid;
		this.demuxer = elementaryStream.demuxer;
		this.startingPesPacket = startingPesPacket;
	}

	ensureBuffered(length: number) {
		const remaining = this.endPos - this.currentPos;
		if (remaining >= length) {
			return length;
		}

		return this.bufferData(length - remaining)
			.then(() => Math.min(this.endPos - this.currentPos, length));
	}

	getCurrentPesPacket() {
		const packet = this.pesPackets[this.currentPesPacketIndex];
		assert(packet);

		return packet;
	}

	async bufferData(length: number): Promise<void> {
		const targetEndPos = this.endPos + length;

		while (this.endPos < targetEndPos) {
			let pesPacket: PesPacket;
			if (this.pesPackets.length === 0) {
				pesPacket = this.startingPesPacket;
			} else {
				// Find the next PES packet
				let currentPos = last(this.pesPackets)!.sectionEndPos;
				assert(currentPos !== null);

				while (true) {
					const packetHeader = await this.demuxer.readPacketHeader(currentPos);
					if (!packetHeader) {
						return;
					}

					if (packetHeader.pid === this.pid) {
						const nextSection = await this.demuxer.readSection(currentPos, true);
						if (!nextSection) {
							return;
						}

						const nextPesPacket = readPesPacket(nextSection, false);
						if (nextPesPacket) {
							pesPacket = nextPesPacket;
							break;
						}
					}

					currentPos += this.demuxer.packetStride;
				}
			}

			this.pesPackets.push(pesPacket);
			this.endPos += pesPacket.data.byteLength;
		}
	}

	readBytes(length: number) {
		const currentPesPacket = this.getCurrentPesPacket();

		const relativeStartOffset = this.currentPos - this.currentPesPacketPos;
		const relativeEndOffset = relativeStartOffset + length;

		this.currentPos += length;

		if (relativeEndOffset <= currentPesPacket.data.byteLength) {
			// Request can be satisfied with one PES packet
			return currentPesPacket.data.subarray(relativeStartOffset, relativeEndOffset);
		}

		// Data spans multiple PES packets, we must do some merging
		const result = new Uint8Array(length);
		result.set(currentPesPacket.data.subarray(relativeStartOffset));
		let offset = currentPesPacket.data.byteLength - relativeStartOffset;

		while (true) {
			this.advanceCurrentPacket();
			const currentPesPacket = this.getCurrentPesPacket();
			const relativeEndOffset = length - offset;

			if (relativeEndOffset <= currentPesPacket.data.byteLength) {
				result.set(currentPesPacket.data.subarray(0, relativeEndOffset), offset);
				break;
			}

			result.set(currentPesPacket.data, offset);
			offset += currentPesPacket.data.byteLength;
		}

		return result;
	}

	readU8() {
		let currentPesPacket = this.getCurrentPesPacket();

		const relativeOffset = this.currentPos - this.currentPesPacketPos;
		this.currentPos++;

		if (relativeOffset < currentPesPacket.data.byteLength) {
			return currentPesPacket.data[relativeOffset]!;
		}

		this.advanceCurrentPacket();

		currentPesPacket = this.getCurrentPesPacket();
		return currentPesPacket.data[0]!;
	}

	seekTo(pos: number) {
		if (pos === this.currentPos) {
			return;
		}

		if (pos < this.currentPos) {
			while (pos < this.currentPesPacketPos) {
				// Move to the previous PES packet
				this.currentPesPacketIndex--;
				const currentPacket = this.getCurrentPesPacket();
				this.currentPesPacketPos -= currentPacket.data.byteLength;
			}
		} else {
			while (true) {
				// Move to the next PES packet
				const currentPesPacket = this.getCurrentPesPacket();
				const currentEndPos = this.currentPesPacketPos + currentPesPacket.data.byteLength;

				if (pos < currentEndPos) {
					break;
				}

				this.currentPesPacketPos += currentPesPacket.data.byteLength;
				this.currentPesPacketIndex++;
			}
		}

		this.currentPos = pos;
	}

	skip(n: number) {
		this.seekTo(this.currentPos + n);
	}

	advanceCurrentPacket() {
		this.currentPesPacketPos += this.getCurrentPesPacket().data.byteLength;
		this.currentPesPacketIndex++;
	}

	async markNextPacket() {
		assert(!this.suppliedPacket);

		const elementaryStream = this.elementaryStream;

		if (elementaryStream.info.type === 'video') {
			const codec = elementaryStream.info.codec;
			const CHUNK_SIZE = 1024;

			if (codec !== 'avc' && codec !== 'hevc') {
				throw new Error('Unhandled.');
			}

			let packetStartPos: number | null = null;

			while (true) {
				let remaining = this.ensureBuffered(CHUNK_SIZE);
				if (remaining instanceof Promise) remaining = await remaining;

				if (remaining === 0) {
					break;
				}

				const chunkStartPos = this.currentPos;
				const chunk = this.readBytes(remaining);
				const length = chunk.byteLength;

				let i = 0;
				while (i < length) {
					const zeroIndex = chunk.indexOf(0, i);
					if (zeroIndex === -1 || zeroIndex >= length) {
						break;
					}
					i = zeroIndex;

					// Check if we have enough bytes to identify a start code
					const posBeforeZero = chunkStartPos + i;

					// Need at least 4 more bytes after the 0x00 to check for start code + NAL type
					if (i + 4 >= length) {
						// Not enough data in current chunk, seek back and let the next iteration handle it
						this.seekTo(posBeforeZero);
						break;
					}

					const b1 = chunk[i + 1]!;
					const b2 = chunk[i + 2]!;
					const b3 = chunk[i + 3]!;

					let startCodeLength = 0;
					let nalUnitTypeByte: number | null = null;

					// Check for 4-byte start code (0x00000001)
					if (b1 === 0x00 && b2 === 0x00 && b3 === 0x01) {
						startCodeLength = 4;
						nalUnitTypeByte = chunk[i + 4]!;
					} else if (b1 === 0x00 && b2 === 0x01) {
						// 3-byte start code (0x000001)
						startCodeLength = 3;
						nalUnitTypeByte = b3;
					}

					if (startCodeLength === 0) {
						// Not a start code, continue
						i++;
						continue;
					}

					const startCodePos = posBeforeZero;

					if (packetStartPos === null) {
						// This is our first start code, mark packet start
						packetStartPos = startCodePos;
						i += startCodeLength;
						continue;
					}

					// We have a second start code. Check if it's an AUD.
					if (nalUnitTypeByte !== null) {
						const nalUnitType = codec === 'avc'
							? extractNalUnitTypeForAvc(nalUnitTypeByte)
							: extractNalUnitTypeForHevc(nalUnitTypeByte);
						const isAud = codec === 'avc'
							? nalUnitType === AvcNalUnitType.AUD
							: nalUnitType === HevcNalUnitType.AUD_NUT;

						if (isAud) {
							// End the packet at this start code (before the AUD)
							const packetLength = startCodePos - packetStartPos;
							this.seekTo(packetStartPos);
							return this.supplyPacket(packetLength, 0);
						}
					}

					// Not an AUD, continue searching
					i += startCodeLength;
				}

				if (remaining < CHUNK_SIZE) {
					// End of stream
					break;
				}
			}

			// End of stream - return remaining data if we have a packet start
			if (packetStartPos !== null) {
				const packetLength = this.endPos - packetStartPos;
				this.seekTo(packetStartPos);
				return this.supplyPacket(packetLength, 0);
			}
		} else {
			const codec = elementaryStream.info.codec;
			const CHUNK_SIZE = 128;

			while (true) {
				let remaining = this.ensureBuffered(CHUNK_SIZE);
				if (remaining instanceof Promise) remaining = await remaining;

				const startPos = this.currentPos;

				while (this.currentPos - startPos < remaining) {
					const byte = this.readU8();

					if (codec === 'aac') {
						if (byte !== 0xff) {
							continue;
						}

						this.skip(-1);
						const possibleHeaderStartPos = this.currentPos;

						let remaining = this.ensureBuffered(MAX_ADTS_FRAME_HEADER_SIZE);
						if (remaining instanceof Promise) remaining = await remaining;

						if (remaining < MAX_ADTS_FRAME_HEADER_SIZE) {
							return;
						}

						const headerBytes = this.readBytes(MAX_ADTS_FRAME_HEADER_SIZE);
						const header = readAdtsFrameHeader(FileSlice.tempFromBytes(headerBytes));

						if (header) {
							this.seekTo(possibleHeaderStartPos);

							let remaining = this.ensureBuffered(header.frameLength);
							if (remaining instanceof Promise) remaining = await remaining;

							return this.supplyPacket(
								remaining,
								Math.round(SAMPLES_PER_AAC_FRAME * TIMESCALE / elementaryStream.info.sampleRate),
							);
						} else {
							this.seekTo(possibleHeaderStartPos + 1);
						}
					} else if (codec === 'mp3') {
						if (byte !== 0xff) {
							continue;
						}

						this.skip(-1);
						const possibleHeaderStartPos = this.currentPos;

						let remaining = this.ensureBuffered(MP3_FRAME_HEADER_SIZE);
						if (remaining instanceof Promise) remaining = await remaining;

						if (remaining < MP3_FRAME_HEADER_SIZE) {
							return;
						}

						const headerBytes = this.readBytes(MP3_FRAME_HEADER_SIZE);
						const word = toDataView(headerBytes).getUint32(0);
						const result = readMp3FrameHeader(word, null);

						if (result.header) {
							this.seekTo(possibleHeaderStartPos);

							let remaining = this.ensureBuffered(result.header.totalSize);
							if (remaining instanceof Promise) remaining = await remaining;

							const duration = result.header.audioSamplesInFrame * TIMESCALE
								/ elementaryStream.info.sampleRate;
							return this.supplyPacket(remaining, Math.round(duration));
						} else {
							this.seekTo(possibleHeaderStartPos + 1);
						}
					} else if (codec === 'ac3') {
						if (byte !== 0x0b) {
							continue;
						}

						this.skip(-1);
						const possibleSyncPos = this.currentPos;

						// Need at least 5 bytes for sync word + CRC + fscod/frmsizecod
						let remaining = this.ensureBuffered(5);
						if (remaining instanceof Promise) remaining = await remaining;

						if (remaining < 5) {
							return;
						}

						const headerBytes = this.readBytes(5);

						// Verify sync word (0x0B77)
						if (headerBytes[0] !== 0x0b || headerBytes[1] !== 0x77) {
							this.seekTo(possibleSyncPos + 1);
							continue;
						}

						const fscod = headerBytes[4]! >> 6;
						const frmsizecod = headerBytes[4]! & 0x3f;

						if (fscod === 3 || frmsizecod > 37) {
							// Invalid
							this.seekTo(possibleSyncPos + 1);
							continue;
						}

						const frameSize = AC3_FRAME_SIZES[3 * frmsizecod + fscod];
						assert(frameSize !== undefined);

						this.seekTo(possibleSyncPos);

						remaining = this.ensureBuffered(frameSize);
						if (remaining instanceof Promise) remaining = await remaining;

						const duration = Math.round(
							AC3_SAMPLES_PER_FRAME * TIMESCALE / elementaryStream.info.sampleRate,
						);
						return this.supplyPacket(remaining, duration);
					} else if (codec === 'eac3') {
						if (byte !== 0x0b) {
							continue;
						}

						this.skip(-1);
						const possibleSyncPos = this.currentPos;

						// Need at least 5 bytes for E-AC-3 header parsing (sync word + frmsiz + fscod/numblkscod)
						let remaining = this.ensureBuffered(5);
						if (remaining instanceof Promise) remaining = await remaining;

						if (remaining < 5) {
							return;
						}

						const headerBytes = this.readBytes(5);

						if (headerBytes[0] !== 0x0b || headerBytes[1] !== 0x77) {
							this.seekTo(possibleSyncPos + 1);
							continue;
						}

						const frmsiz = ((headerBytes[2]! & 0x07) << 8) | headerBytes[3]!;
						const frameSize = (frmsiz + 1) * 2;
						const fscod = headerBytes[4]! >> 6;
						const numblkscod = fscod === 3 ? 3 : (headerBytes[4]! >> 4) & 0x03;
						const numblks = EAC3_NUMBLKS_TABLE[numblkscod]!;

						this.seekTo(possibleSyncPos);

						remaining = this.ensureBuffered(frameSize);
						if (remaining instanceof Promise) remaining = await remaining;

						// Duration = numblks * 256 samples per block
						const samplesPerFrame = numblks * 256;
						const duration = Math.round(
							samplesPerFrame * TIMESCALE / elementaryStream.info.sampleRate,
						);
						return this.supplyPacket(remaining, duration);
					} else {
						throw new Error('Unhandled.');
					}
				}

				if (remaining < CHUNK_SIZE) {
					break;
				}
			}
		}
	}

	/** Supplies the context with a new encoded packet, beginning at the current position. */
	supplyPacket(packetLength: number, intrinsicDuration: number) {
		const currentPesPacket = this.getCurrentPesPacket();

		let pts: number;
		if (this.lastSuppliedPesPacket === currentPesPacket) {
			assert(this.nextPts !== null);
			pts = this.nextPts;
		} else {
			if (currentPesPacket.pts === null) {
				throw new Error(MISSING_PTS_ERROR_MESSAGE);
			}

			pts = currentPesPacket.pts;

			maybeInsertReferencePacket(this.elementaryStream, currentPesPacket as TimestampedPesPacket);
		}

		this.lastSuppliedPesPacket = currentPesPacket;
		this.nextPts = pts + intrinsicDuration;

		const sectionStartPos = currentPesPacket.sectionStartPos;
		// The sequence number is the starting position of the section the PES packet is in, PLUS the offset within the
		// PES packet where the packet starts.
		const sequenceNumber = sectionStartPos + (this.currentPos - this.currentPesPacketPos);
		const data = this.readBytes(packetLength);

		let randomAccessIndicator = currentPesPacket.randomAccessIndicator;

		if (randomAccessIndicator === 0 && !this.elementaryStream.canBeTrustedWithKeyPackets) {
			if (this.elementaryStream.info.type === 'audio') {
				randomAccessIndicator = 1;
			} else {
				if (this.elementaryStream.info.decoderConfig) {
					const isKey = determineVideoPacketType(
						this.elementaryStream.info.codec,
						this.elementaryStream.info.decoderConfig,
						data,
					) === 'key';

					randomAccessIndicator = Number(isKey);
				} else {
					// We're reading packets before the decoder config is determined
				}
			}
		}

		this.suppliedPacket = {
			pts,
			data,
			sequenceNumber,
			sectionStartPos,
			randomAccessIndicator,
		};

		this.pesPackets.splice(0, this.currentPesPacketIndex);
		this.currentPesPacketIndex = 0;
	}
}

/**
 * A buffer that simulates decoder frame reordering to compute packet durations. Packets arrive in decode order but
 * durations are based on presentation order.
 */
class PacketBuffer {
	backing: MpegTsTrackBacking;
	context: PacketReadingContext;

	decodeOrderPackets: SuppliedPacket[] = [];
	reorderSize: number;
	reorderBuffer: SuppliedPacket[] = [];
	presentationOrderPackets: SuppliedPacket[] = [];
	reachedEnd = false;
	lastDuration = 0;

	constructor(backing: MpegTsTrackBacking, context: PacketReadingContext) {
		this.backing = backing;
		this.context = context;
		this.reorderSize = backing.getReorderSize();
		assert(this.reorderSize >= 0);
	}

	async readNext(): Promise<{ packet: SuppliedPacket; duration: number } | null> {
		if (this.decodeOrderPackets.length === 0) {
			// We need the next packet
			const didRead = await this.readNextPacket();
			if (!didRead) {
				return null;
			}
		}

		// Ensure we know the next packet in presentation order so we can compute the current packet's duration
		await this.ensureCurrentPacketHasNext();

		const packet = this.decodeOrderPackets[0]!;

		// Let's compute the duration
		const presentationIndex = this.presentationOrderPackets.indexOf(packet);
		assert(presentationIndex !== -1);

		let duration: number;
		if (presentationIndex === this.presentationOrderPackets.length - 1) {
			duration = this.lastDuration; // Reasonable heuristic
		} else {
			const nextPacket = this.presentationOrderPackets[presentationIndex + 1]!;
			duration = nextPacket.pts - packet.pts;
			this.lastDuration = duration;
		}

		this.decodeOrderPackets.shift();

		// Shrink the presentation array as much as possible
		while (this.presentationOrderPackets.length > 0) {
			const first = this.presentationOrderPackets[0]!;
			if (this.decodeOrderPackets.includes(first)) {
				break;
			}

			this.presentationOrderPackets.shift();
		}

		return { packet, duration };
	}

	async readNextPacket() {
		if (this.reachedEnd) {
			return false;
		}

		let suppliedPacket: SuppliedPacket | null;
		if (this.context.suppliedPacket) {
			// Small optimization: there was already a supplied packet in the context, so let's first use that one
			suppliedPacket = this.context.suppliedPacket;
		} else {
			await this.context.markNextPacket();
			suppliedPacket = this.context.suppliedPacket;
		}
		this.context.suppliedPacket = null;

		if (!suppliedPacket) {
			this.reachedEnd = true;
			this.flushReorderBuffer();

			return false;
		}

		this.decodeOrderPackets.push(suppliedPacket);
		this.processPacketThroughReorderBuffer(suppliedPacket);

		return true;
	}

	async ensureCurrentPacketHasNext() {
		const current = this.decodeOrderPackets[0];
		assert(current);

		while (true) {
			const presentationIndex = this.presentationOrderPackets.indexOf(current);

			// Check if current packet has a next packet
			if (presentationIndex !== -1 && presentationIndex <= this.presentationOrderPackets.length - 2) {
				break;
			}

			const didRead = await this.readNextPacket();
			if (!didRead) {
				break;
			}
		}
	}

	processPacketThroughReorderBuffer(packet: SuppliedPacket) {
		this.reorderBuffer.push(packet);

		// If buffer is now overfull, output the packet with smallest PTS
		if (this.reorderBuffer.length > this.reorderSize) {
			let minIndex = 0;
			for (let i = 1; i < this.reorderBuffer.length; i++) {
				if (this.reorderBuffer[i]!.pts < this.reorderBuffer[minIndex]!.pts) {
					minIndex = i;
				}
			}

			const packet = this.reorderBuffer[minIndex]!;
			this.presentationOrderPackets.push(packet);
			this.reorderBuffer.splice(minIndex, 1);
		}
	}

	flushReorderBuffer() {
		this.reorderBuffer.sort((a, b) => a.pts - b.pts);
		this.presentationOrderPackets.push(...this.reorderBuffer);
		this.reorderBuffer.length = 0;
	}
}
