UNPKG

mediabunny

Version:

Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.

986 lines (817 loc) 29.4 kB
/*! * Copyright (c) 2025-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 { OPUS_INTERNAL_SAMPLE_RATE } from '../codec'; import { parseModesFromVorbisSetupPacket, parseOpusIdentificationHeader } from '../codec-data'; import { Demuxer } from '../demuxer'; import { Input } from '../input'; import { InputAudioTrack, InputAudioTrackBacking } from '../input-track'; import { PacketRetrievalOptions } from '../media-sink'; import { assert, AsyncMutex, findLast, roundToPrecision, toDataView, UNDETERMINED_LANGUAGE } from '../misc'; import { EncodedPacket, PLACEHOLDER_DATA } from '../packet'; import { Reader } from '../reader'; import { buildOggMimeType, computeOggPageCrc, extractSampleMetadata, OggCodecInfo } from './ogg-misc'; import { MAX_PAGE_HEADER_SIZE, MAX_PAGE_SIZE, MIN_PAGE_HEADER_SIZE, OggReader, Page } from './ogg-reader'; type LogicalBitstream = { serialNumber: number; bosPage: Page; description: Uint8Array | null; numberOfChannels: number; sampleRate: number; codecInfo: OggCodecInfo; lastMetadataPacket: Packet | null; }; type Packet = { data: Uint8Array; endPage: Page; endSegmentIndex: number; }; export class OggDemuxer extends Demuxer { reader: OggReader; /** * Lots of reading operations require multiple async reads and thus need to be mutually exclusive to avoid * conflicts in reader position. */ readingMutex = new AsyncMutex(); metadataPromise: Promise<void> | null = null; fileSize: number | null = null; bitstreams: LogicalBitstream[] = []; tracks: InputAudioTrack[] = []; constructor(input: Input) { super(input); // We don't need a persistent metadata reader as we read all metadata once at the start and then never again this.reader = new OggReader(new Reader(input.source, 64 * 2 ** 20)); } async readMetadata() { return this.metadataPromise ??= (async () => { this.fileSize = await this.input.source.getSize(); while (this.reader.pos < this.fileSize - MIN_PAGE_HEADER_SIZE) { await this.reader.reader.loadRange( this.reader.pos, this.reader.pos + MAX_PAGE_HEADER_SIZE, ); const page = this.reader.readPageHeader(); if (!page) { break; } const isBos = !!(page.headerType & 0x02); if (!isBos) { // All bos pages for all bitstreams are required to be at the start, so if the page is not bos then // we know we've seen all bitstreams (minus chaining) break; } this.bitstreams.push({ serialNumber: page.serialNumber, bosPage: page, description: null, numberOfChannels: -1, sampleRate: -1, codecInfo: { codec: null, vorbisInfo: null, opusInfo: null, }, lastMetadataPacket: null, }); this.reader.pos = page.headerStartPos + page.totalSize; } for (const bitstream of this.bitstreams) { const firstPacket = await this.readPacket(this.reader, bitstream.bosPage, 0); if (!firstPacket) { continue; } if ( // Check for Vorbis firstPacket.data.byteLength >= 7 && firstPacket.data[0] === 0x01 // Packet type 1 = identification header && firstPacket.data[1] === 0x76 // 'v' && firstPacket.data[2] === 0x6f // 'o' && firstPacket.data[3] === 0x72 // 'r' && firstPacket.data[4] === 0x62 // 'b' && firstPacket.data[5] === 0x69 // 'i' && firstPacket.data[6] === 0x73 // 's' ) { await this.readVorbisMetadata(firstPacket, bitstream); } else if ( // Check for Opus firstPacket.data.byteLength >= 8 && firstPacket.data[0] === 0x4f // 'O' && firstPacket.data[1] === 0x70 // 'p' && firstPacket.data[2] === 0x75 // 'u' && firstPacket.data[3] === 0x73 // 's' && firstPacket.data[4] === 0x48 // 'H' && firstPacket.data[5] === 0x65 // 'e' && firstPacket.data[6] === 0x61 // 'a' && firstPacket.data[7] === 0x64 // 'd' ) { await this.readOpusMetadata(firstPacket, bitstream); } if (bitstream.codecInfo.codec !== null) { this.tracks.push(new InputAudioTrack(new OggAudioTrackBacking(bitstream, this))); } } })(); } async readVorbisMetadata(firstPacket: Packet, bitstream: LogicalBitstream) { let nextPacketPosition = await this.findNextPacketStart(this.reader, firstPacket); if (!nextPacketPosition) { return; } const secondPacket = await this.readPacket( this.reader, nextPacketPosition.startPage, nextPacketPosition.startSegmentIndex, ); if (!secondPacket) { return; } nextPacketPosition = await this.findNextPacketStart(this.reader, secondPacket); if (!nextPacketPosition) { return; } const thirdPacket = await this.readPacket( this.reader, nextPacketPosition.startPage, nextPacketPosition.startSegmentIndex, ); if (!thirdPacket) { return; } if (secondPacket.data[0] !== 0x03 || thirdPacket.data[0] !== 0x05) { return; } const lacingValues: number[] = []; const addBytesToSegmentTable = (bytes: number) => { while (true) { lacingValues.push(Math.min(255, bytes)); if (bytes < 255) { break; } bytes -= 255; } }; addBytesToSegmentTable(firstPacket.data.length); addBytesToSegmentTable(secondPacket.data.length); // We don't add the last packet to the segment table, as it is assumed to be whatever bytes remain const description = new Uint8Array( 1 + lacingValues.length + firstPacket.data.length + secondPacket.data.length + thirdPacket.data.length, ); description[0] = lacingValues.length; description.set( lacingValues, 1, ); description.set( firstPacket.data, 1 + lacingValues.length, ); description.set( secondPacket.data, 1 + lacingValues.length + firstPacket.data.length, ); description.set( thirdPacket.data, 1 + lacingValues.length + firstPacket.data.length + secondPacket.data.length, ); bitstream.codecInfo.codec = 'vorbis'; bitstream.description = description; bitstream.lastMetadataPacket = thirdPacket; const view = toDataView(firstPacket.data); bitstream.numberOfChannels = view.getUint8(11); bitstream.sampleRate = view.getUint32(12, true); const blockSizeByte = view.getUint8(28); bitstream.codecInfo.vorbisInfo = { blocksizes: [ 1 << (blockSizeByte & 0xf), 1 << (blockSizeByte >> 4), ], modeBlockflags: parseModesFromVorbisSetupPacket(thirdPacket.data).modeBlockflags, }; } async readOpusMetadata(firstPacket: Packet, bitstream: LogicalBitstream) { // From https://datatracker.ietf.org/doc/html/rfc7845#section-5: // "An Ogg Opus logical stream contains exactly two mandatory header packets: an identification header and a // comment header." const nextPacketPosition = await this.findNextPacketStart(this.reader, firstPacket); if (!nextPacketPosition) { return; } const secondPacket = await this.readPacket( this.reader, nextPacketPosition.startPage, nextPacketPosition.startSegmentIndex, ); if (!secondPacket) { return; } // We don't make use of the comment header's data bitstream.codecInfo.codec = 'opus'; bitstream.description = firstPacket.data; bitstream.lastMetadataPacket = secondPacket; const header = parseOpusIdentificationHeader(firstPacket.data); bitstream.numberOfChannels = header.outputChannelCount; bitstream.sampleRate = header.inputSampleRate; bitstream.codecInfo.opusInfo = { preSkip: header.preSkip, }; } async readPacket(reader: OggReader, startPage: Page, startSegmentIndex: number): Promise<Packet | null> { assert(startSegmentIndex < startPage.lacingValues.length); assert(this.fileSize); let startDataOffset = 0; for (let i = 0; i < startSegmentIndex; i++) { startDataOffset += startPage.lacingValues[i]!; } let currentPage: Page = startPage; let currentDataOffset = startDataOffset; let currentSegmentIndex = startSegmentIndex; const chunks: Uint8Array[] = []; outer: while (true) { // Load the entire page data await reader.reader.loadRange( currentPage.dataStartPos, currentPage.dataStartPos + currentPage.dataSize, ); reader.pos = currentPage.dataStartPos; const pageData = reader.readBytes(currentPage.dataSize); while (true) { if (currentSegmentIndex === currentPage.lacingValues.length) { chunks.push(pageData.subarray(startDataOffset, currentDataOffset)); break; } const lacingValue = currentPage.lacingValues[currentSegmentIndex]!; currentDataOffset += lacingValue; if (lacingValue < 255) { chunks.push(pageData.subarray(startDataOffset, currentDataOffset)); break outer; } currentSegmentIndex++; } // The packet extends to the next page; let's find it while (true) { reader.pos = currentPage.headerStartPos + currentPage.totalSize; if (reader.pos >= this.fileSize - MIN_PAGE_HEADER_SIZE) { return null; } await reader.reader.loadRange(reader.pos, reader.pos + MAX_PAGE_HEADER_SIZE); const nextPage = reader.readPageHeader(); if (!nextPage) { return null; } currentPage = nextPage; if (currentPage.serialNumber === startPage.serialNumber) { break; } } startDataOffset = 0; currentDataOffset = 0; currentSegmentIndex = 0; } const totalPacketSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0); const packetData = new Uint8Array(totalPacketSize); let offset = 0; for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]!; packetData.set(chunk, offset); offset += chunk.length; } return { data: packetData, endPage: currentPage, endSegmentIndex: currentSegmentIndex, }; } async findNextPacketStart(reader: OggReader, lastPacket: Packet) { assert(this.fileSize !== null); // If there's another segment in the same page, return it if (lastPacket.endSegmentIndex < lastPacket.endPage.lacingValues.length - 1) { return { startPage: lastPacket.endPage, startSegmentIndex: lastPacket.endSegmentIndex + 1 }; } const isEos = !!(lastPacket.endPage.headerType & 0x04); if (isEos) { // The page is marked as the last page of the logical bitstream, so we won't find anything beyond it return null; } // Otherwise, search for the next page belonging to the same bitstream reader.pos = lastPacket.endPage.headerStartPos + lastPacket.endPage.totalSize; while (true) { if (reader.pos >= this.fileSize - MIN_PAGE_HEADER_SIZE) { return null; } await reader.reader.loadRange(reader.pos, reader.pos + MAX_PAGE_HEADER_SIZE); const nextPage = reader.readPageHeader(); if (!nextPage) { return null; } if (nextPage.serialNumber === lastPacket.endPage.serialNumber) { return { startPage: nextPage, startSegmentIndex: 0 }; } reader.pos = nextPage.headerStartPos + nextPage.totalSize; } } async getMimeType() { await this.readMetadata(); const codecStrings = await Promise.all(this.tracks.map(x => x.getCodecParameterString())); return buildOggMimeType({ codecStrings: codecStrings.filter(Boolean) as string[], }); } async getTracks() { await this.readMetadata(); return this.tracks; } async computeDuration() { const tracks = await this.getTracks(); const trackDurations = await Promise.all(tracks.map(x => x.computeDuration())); return Math.max(0, ...trackDurations); } } type EncodedPacketMetadata = { packet: Packet; timestampInSamples: number; durationInSamples: number; vorbisBlockSize: number | null; }; class OggAudioTrackBacking implements InputAudioTrackBacking { internalSampleRate: number; encodedPacketToMetadata = new WeakMap<EncodedPacket, EncodedPacketMetadata>(); constructor(public bitstream: LogicalBitstream, public demuxer: OggDemuxer) { // Opus always uses a fixed sample rate for its internal calculations, even if the actual rate is different this.internalSampleRate = bitstream.codecInfo.codec === 'opus' ? OPUS_INTERNAL_SAMPLE_RATE : bitstream.sampleRate; } getId() { return this.bitstream.serialNumber; } getNumberOfChannels() { return this.bitstream.numberOfChannels; } getSampleRate() { return this.bitstream.sampleRate; } getTimeResolution() { return this.bitstream.sampleRate; } getCodec() { return this.bitstream.codecInfo.codec; } async getDecoderConfig(): Promise<AudioDecoderConfig | null> { assert(this.bitstream.codecInfo.codec); return { codec: this.bitstream.codecInfo.codec, numberOfChannels: this.bitstream.numberOfChannels, sampleRate: this.bitstream.sampleRate, description: this.bitstream.description ?? undefined, }; } getLanguageCode() { return UNDETERMINED_LANGUAGE; } async getFirstTimestamp() { return 0; } async computeDuration() { const lastPacket = await this.getPacket(Infinity, { metadataOnly: true }); return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0); } granulePositionToTimestampInSamples(granulePosition: number) { if (this.bitstream.codecInfo.codec === 'opus') { assert(this.bitstream.codecInfo.opusInfo); return granulePosition - this.bitstream.codecInfo.opusInfo.preSkip; } return granulePosition; } createEncodedPacketFromOggPacket( packet: Packet | null, additional: { timestampInSamples: number; vorbisLastBlocksize: number | null; }, options: PacketRetrievalOptions, ) { if (!packet) { return null; } const { durationInSamples, vorbisBlockSize } = extractSampleMetadata( packet.data, this.bitstream.codecInfo, additional.vorbisLastBlocksize, ); const encodedPacket = new EncodedPacket( options.metadataOnly ? PLACEHOLDER_DATA : packet.data, 'key', Math.max(0, additional.timestampInSamples) / this.internalSampleRate, durationInSamples / this.internalSampleRate, packet.endPage.headerStartPos + packet.endSegmentIndex, packet.data.byteLength, ); this.encodedPacketToMetadata.set(encodedPacket, { packet, timestampInSamples: additional.timestampInSamples, durationInSamples, vorbisBlockSize, }); return encodedPacket; } async getFirstPacket(options: PacketRetrievalOptions, exclusive = true) { const release = exclusive ? await this.demuxer.readingMutex.acquire() : null; try { assert(this.bitstream.lastMetadataPacket); const packetPosition = await this.demuxer.findNextPacketStart( this.demuxer.reader, this.bitstream.lastMetadataPacket, ); if (!packetPosition) { return null; } let timestampInSamples = 0; if (this.bitstream.codecInfo.codec === 'opus') { assert(this.bitstream.codecInfo.opusInfo); timestampInSamples -= this.bitstream.codecInfo.opusInfo.preSkip; } const packet = await this.demuxer.readPacket( this.demuxer.reader, packetPosition.startPage, packetPosition.startSegmentIndex, ); return this.createEncodedPacketFromOggPacket( packet, { timestampInSamples, vorbisLastBlocksize: null, }, options, ); } finally { release?.(); } } async getNextPacket(prevPacket: EncodedPacket, options: PacketRetrievalOptions) { const release = await this.demuxer.readingMutex.acquire(); try { const prevMetadata = this.encodedPacketToMetadata.get(prevPacket); if (!prevMetadata) { throw new Error('Packet was not created from this track.'); } const packetPosition = await this.demuxer.findNextPacketStart(this.demuxer.reader, prevMetadata.packet); if (!packetPosition) { return null; } const timestampInSamples = prevMetadata.timestampInSamples + prevMetadata.durationInSamples; const packet = await this.demuxer.readPacket( this.demuxer.reader, packetPosition.startPage, packetPosition.startSegmentIndex, ); return this.createEncodedPacketFromOggPacket( packet, { timestampInSamples, vorbisLastBlocksize: prevMetadata.vorbisBlockSize, }, options, ); } finally { release(); } } async getPacket(timestamp: number, options: PacketRetrievalOptions) { const release = await this.demuxer.readingMutex.acquire(); try { assert(this.demuxer.fileSize !== null); const timestampInSamples = roundToPrecision(timestamp * this.internalSampleRate, 14); if (timestampInSamples === 0) { // Fast path for timestamp 0 - avoids binary search when playing back from the start return this.getFirstPacket(options, false); } if (timestampInSamples < 0) { // There's nothing here return null; } const reader = this.demuxer.reader; assert(this.bitstream.lastMetadataPacket); const startPosition = await this.demuxer.findNextPacketStart( reader, this.bitstream.lastMetadataPacket, ); if (!startPosition) { return null; } let lowPage = startPosition.startPage; let high = this.demuxer.fileSize; const lowPages: Page[] = [lowPage]; // First, let's perform a binary serach (bisection search) on the file to find the approximate page where // we'll find the packet. We want to find a page whose end packet position is less than or equal to the // packet position we're searching for. // Outer loop: Does the binary serach outer: while (lowPage.headerStartPos + lowPage.totalSize < high) { const low = lowPage.headerStartPos; const mid = Math.floor((low + high) / 2); let searchStartPos = mid; // Inner loop: Does a linear forward scan if the page cannot be found immediately while (true) { const until = Math.min( searchStartPos + MAX_PAGE_SIZE, high - MIN_PAGE_HEADER_SIZE, ); await reader.reader.loadRange(searchStartPos, until); reader.pos = searchStartPos; const found = reader.findNextPageHeader(until); if (!found) { high = mid + MIN_PAGE_HEADER_SIZE; continue outer; } await reader.reader.loadRange(reader.pos, reader.pos + MAX_PAGE_HEADER_SIZE); const page = reader.readPageHeader(); assert(page); let pageValid = false; if (page.serialNumber === this.bitstream.serialNumber) { // Serial numbers are basically random numbers, and the chance of finding a fake page with // matching serial number is astronomically low, so we can be pretty sure this page is legit. pageValid = true; } else { await reader.reader.loadRange(page.headerStartPos, page.headerStartPos + page.totalSize); // Validate the page by checking checksum reader.pos = page.headerStartPos; const bytes = reader.readBytes(page.totalSize); const crc = computeOggPageCrc(bytes); pageValid = crc === page.checksum; } if (!pageValid) { // Keep searching for a valid page searchStartPos = page.headerStartPos + 4; // 'OggS' is 4 bytes continue; } if (pageValid && page.serialNumber !== this.bitstream.serialNumber) { // Page is valid but from a different bitstream, so keep searching forward until we find one // belonging to the our bitstream searchStartPos = page.headerStartPos + page.totalSize; continue; } const isContinuationPage = page.granulePosition === -1; if (isContinuationPage) { // No packet ends on this page - keep looking searchStartPos = page.headerStartPos + page.totalSize; continue; } // The page is valid and belongs to our bitstream; let's check its granule position to see where we // need to take the bisection search. if (this.granulePositionToTimestampInSamples(page.granulePosition) > timestampInSamples) { high = page.headerStartPos; } else { lowPage = page; lowPages.push(page); } continue outer; } } // Now we have the last page with a packet position <= the packet position we're looking for, but there // might be multiple pages with the packet position, in which case we actually need to find the first of // such pages. We'll do this in two steps: First, let's find the latest page we know with an earlier packet // position, and then linear scan ourselves forward until we find the correct page. let lowerPage = startPosition.startPage; for (const otherLowPage of lowPages) { if (otherLowPage.granulePosition === lowPage.granulePosition) { break; } if (!lowerPage || otherLowPage.headerStartPos > lowerPage.headerStartPos) { lowerPage = otherLowPage; } } let currentPage: Page | null = lowerPage; // Keep track of the pages we traversed, we need these later for backwards seeking const previousPages: Page[] = [currentPage]; while (true) { // This loop must terminate as we'll eventually reach lowPage if ( currentPage.serialNumber === this.bitstream.serialNumber && currentPage.granulePosition === lowPage.granulePosition ) { break; } reader.pos = currentPage.headerStartPos + currentPage.totalSize; await reader.reader.loadRange(reader.pos, reader.pos + MAX_PAGE_HEADER_SIZE); const nextPage = reader.readPageHeader(); assert(nextPage); currentPage = nextPage; if (currentPage.serialNumber === this.bitstream.serialNumber) { previousPages.push(currentPage); } } assert(currentPage.granulePosition !== -1); let currentSegmentIndex: number | null = null; let currentTimestampInSamples: number; let currentTimestampIsCorrect: boolean; // These indicate the end position of the packet that the granule position belongs to let endPage = currentPage; let endSegmentIndex = 0; if (currentPage.headerStartPos === startPosition.startPage.headerStartPos) { currentTimestampInSamples = this.granulePositionToTimestampInSamples(0); currentTimestampIsCorrect = true; currentSegmentIndex = 0; } else { currentTimestampInSamples = 0; // Placeholder value! We'll refine it once we can currentTimestampIsCorrect = false; // Find the segment index of the next packet for (let i = currentPage.lacingValues.length - 1; i >= 0; i--) { const value = currentPage.lacingValues[i]!; if (value < 255) { // We know the last packet ended at i, so the next one starts at i + 1 currentSegmentIndex = i + 1; break; } } // This must hold: Since this page has a granule position set, that means there must be a packet that // ends in this page. if (currentSegmentIndex === null) { throw new Error('Invalid page with granule position: no packets end on this page.'); } endSegmentIndex = currentSegmentIndex - 1; const pseudopacket: Packet = { data: PLACEHOLDER_DATA, endPage, endSegmentIndex, }; const nextPosition = await this.demuxer.findNextPacketStart(reader, pseudopacket); if (nextPosition) { // Let's rewind a single step (packet) - this previous packet ensures that we'll correctly compute // the duration for the packet we're looking for. const endPosition = findPreviousPacketEndPosition(previousPages, currentPage, currentSegmentIndex); assert(endPosition); const startPosition = findPacketStartPosition( previousPages, endPosition.page, endPosition.segmentIndex, ); if (startPosition) { currentPage = startPosition.page; currentSegmentIndex = startPosition.segmentIndex; } } else { // There is no next position, which means we're looking for the last packet in the bitstream. The // granule position on the last page tends to be fucky, so let's instead start the search on the // page before that. So let's loop until we find a packet that ends in a previous page. while (true) { const endPosition = findPreviousPacketEndPosition( previousPages, currentPage, currentSegmentIndex, ); if (!endPosition) { break; } const startPosition = findPacketStartPosition( previousPages, endPosition.page, endPosition.segmentIndex, ); if (!startPosition) { break; } currentPage = startPosition.page; currentSegmentIndex = startPosition.segmentIndex; if (endPosition.page.headerStartPos !== endPage.headerStartPos) { endPage = endPosition.page; endSegmentIndex = endPosition.segmentIndex; break; } } } } let lastEncodedPacket: EncodedPacket | null = null; let lastEncodedPacketMetadata: EncodedPacketMetadata | null = null; // Alright, now it's time for the final, granular seek: We keep iterating over packets until we've found the // one with the correct timestamp - i.e., the last one with a timestamp <= the timestamp we're looking for. while (currentPage !== null) { assert(currentSegmentIndex !== null); const packet = await this.demuxer.readPacket(reader, currentPage, currentSegmentIndex); if (!packet) { break; } // We might need to skip the packet if it's a metadata one const skipPacket = currentPage.headerStartPos === startPosition.startPage.headerStartPos && currentSegmentIndex < startPosition.startSegmentIndex; if (!skipPacket) { let encodedPacket = this.createEncodedPacketFromOggPacket( packet, { timestampInSamples: currentTimestampInSamples, vorbisLastBlocksize: lastEncodedPacketMetadata?.vorbisBlockSize ?? null, }, options, ); assert(encodedPacket); let encodedPacketMetadata = this.encodedPacketToMetadata.get(encodedPacket); assert(encodedPacketMetadata); if ( !currentTimestampIsCorrect && packet.endPage.headerStartPos === endPage.headerStartPos && packet.endSegmentIndex === endSegmentIndex ) { // We know this packet end timestamp can be derived from the page's granule position currentTimestampInSamples = this.granulePositionToTimestampInSamples( currentPage.granulePosition, ); currentTimestampIsCorrect = true; // Let's backpatch the packet we just created with the correct timestamp encodedPacket = this.createEncodedPacketFromOggPacket( packet, { timestampInSamples: currentTimestampInSamples - encodedPacketMetadata.durationInSamples, vorbisLastBlocksize: lastEncodedPacketMetadata?.vorbisBlockSize ?? null, }, options, ); assert(encodedPacket); encodedPacketMetadata = this.encodedPacketToMetadata.get(encodedPacket); assert(encodedPacketMetadata); } else { currentTimestampInSamples += encodedPacketMetadata.durationInSamples; } lastEncodedPacket = encodedPacket; lastEncodedPacketMetadata = encodedPacketMetadata; if ( currentTimestampIsCorrect && ( // Next timestamp will be too late Math.max(currentTimestampInSamples, 0) > timestampInSamples // This timestamp already matches || Math.max(encodedPacketMetadata.timestampInSamples, 0) === timestampInSamples ) ) { break; } } const nextPosition = await this.demuxer.findNextPacketStart(reader, packet); if (!nextPosition) { break; } currentPage = nextPosition.startPage; currentSegmentIndex = nextPosition.startSegmentIndex; } return lastEncodedPacket; } finally { release(); } } getKeyPacket(timestamp: number, options: PacketRetrievalOptions) { return this.getPacket(timestamp, options); } getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions) { return this.getNextPacket(packet, options); } } /** Finds the start position of a packet given its end position. */ const findPacketStartPosition = (pageList: Page[], endPage: Page, endSegmentIndex: number) => { let page = endPage; let segmentIndex = endSegmentIndex; outer: while (true) { segmentIndex--; for (segmentIndex; segmentIndex >= 0; segmentIndex--) { const lacingValue = page.lacingValues[segmentIndex]!; if (lacingValue < 255) { segmentIndex++; // We know the last packet starts here break outer; } } assert(segmentIndex === -1); const pageStartsWithFreshPacket = !(page.headerType & 0x01); if (pageStartsWithFreshPacket) { // Fast exit: We know we don't need to look in the previous page segmentIndex = 0; break; } const previousPage = findLast( pageList, x => x.headerStartPos < page.headerStartPos, ); if (!previousPage) { return null; } page = previousPage; segmentIndex = page.lacingValues.length; } assert(segmentIndex !== -1); if (segmentIndex === page.lacingValues.length) { // Wrap back around to the first segment of the next page const nextPage = pageList[pageList.indexOf(page) + 1]; assert(nextPage); page = nextPage; segmentIndex = 0; } return { page, segmentIndex }; }; /** Finds the end position of a packet given the start position of the following packet. */ const findPreviousPacketEndPosition = (pageList: Page[], startPage: Page, startSegmentIndex: number) => { if (startSegmentIndex > 0) { // Easy return { page: startPage, segmentIndex: startSegmentIndex - 1 }; } const previousPage = findLast( pageList, x => x.headerStartPos < startPage.headerStartPos, ); if (!previousPage) { return null; } return { page: previousPage, segmentIndex: previousPage.lacingValues.length - 1 }; };