mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-02 05:13:50 +02:00
Add new slice-based reader system, port all demuxers to new reader system, add optimized BlobSource
This commit is contained in:
+371
-389
@@ -12,11 +12,18 @@ 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 { assert, findLast, roundToPrecision, toDataView, UNDETERMINED_LANGUAGE } from '../misc';
|
||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||
import { Reader } from '../reader';
|
||||
import { readBytes, Reader2 } from '../reader2';
|
||||
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';
|
||||
import {
|
||||
findNextPageHeader,
|
||||
MAX_PAGE_HEADER_SIZE,
|
||||
MAX_PAGE_SIZE,
|
||||
MIN_PAGE_HEADER_SIZE,
|
||||
Page,
|
||||
readPageHeader,
|
||||
} from './ogg-reader';
|
||||
|
||||
type LogicalBitstream = {
|
||||
serialNumber: number;
|
||||
@@ -36,12 +43,7 @@ type Packet = {
|
||||
};
|
||||
|
||||
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();
|
||||
reader: Reader2;
|
||||
|
||||
metadataPromise: Promise<void> | null = null;
|
||||
fileSize: number | null = null;
|
||||
@@ -51,21 +53,22 @@ export class OggDemuxer extends Demuxer {
|
||||
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));
|
||||
this.reader = input._reader2;
|
||||
}
|
||||
|
||||
async readMetadata() {
|
||||
return this.metadataPromise ??= (async () => {
|
||||
this.fileSize = await this.input.source.getSize();
|
||||
let fileSize = this.reader.requestSize();
|
||||
if (fileSize instanceof Promise) fileSize = await fileSize;
|
||||
this.fileSize = fileSize;
|
||||
|
||||
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,
|
||||
);
|
||||
let currentPos = 0;
|
||||
while (currentPos <= this.fileSize - MIN_PAGE_HEADER_SIZE) {
|
||||
let slice = this.reader.requestSliceRange(currentPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) break;
|
||||
|
||||
const page = this.reader.readPageHeader();
|
||||
const page = readPageHeader(slice);
|
||||
if (!page) {
|
||||
break;
|
||||
}
|
||||
@@ -91,11 +94,11 @@ export class OggDemuxer extends Demuxer {
|
||||
lastMetadataPacket: null,
|
||||
});
|
||||
|
||||
this.reader.pos = page.headerStartPos + page.totalSize;
|
||||
currentPos = page.headerStartPos + page.totalSize;
|
||||
}
|
||||
|
||||
for (const bitstream of this.bitstreams) {
|
||||
const firstPacket = await this.readPacket(this.reader, bitstream.bosPage, 0);
|
||||
const firstPacket = await this.readPacket(bitstream.bosPage, 0);
|
||||
if (!firstPacket) {
|
||||
continue;
|
||||
}
|
||||
@@ -135,30 +138,22 @@ export class OggDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
async readVorbisMetadata(firstPacket: Packet, bitstream: LogicalBitstream) {
|
||||
let nextPacketPosition = await this.findNextPacketStart(this.reader, firstPacket);
|
||||
let nextPacketPosition = await this.findNextPacketStart(firstPacket);
|
||||
if (!nextPacketPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const secondPacket = await this.readPacket(
|
||||
this.reader,
|
||||
nextPacketPosition.startPage,
|
||||
nextPacketPosition.startSegmentIndex,
|
||||
);
|
||||
const secondPacket = await this.readPacket(nextPacketPosition.startPage, nextPacketPosition.startSegmentIndex);
|
||||
if (!secondPacket) {
|
||||
return;
|
||||
}
|
||||
|
||||
nextPacketPosition = await this.findNextPacketStart(this.reader, secondPacket);
|
||||
nextPacketPosition = await this.findNextPacketStart(secondPacket);
|
||||
if (!nextPacketPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const thirdPacket = await this.readPacket(
|
||||
this.reader,
|
||||
nextPacketPosition.startPage,
|
||||
nextPacketPosition.startSegmentIndex,
|
||||
);
|
||||
const thirdPacket = await this.readPacket(nextPacketPosition.startPage, nextPacketPosition.startSegmentIndex);
|
||||
if (!thirdPacket) {
|
||||
return;
|
||||
}
|
||||
@@ -224,13 +219,12 @@ export class OggDemuxer extends Demuxer {
|
||||
// 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);
|
||||
const nextPacketPosition = await this.findNextPacketStart(firstPacket);
|
||||
if (!nextPacketPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const secondPacket = await this.readPacket(
|
||||
this.reader,
|
||||
nextPacketPosition.startPage,
|
||||
nextPacketPosition.startSegmentIndex,
|
||||
);
|
||||
@@ -253,7 +247,7 @@ export class OggDemuxer extends Demuxer {
|
||||
};
|
||||
}
|
||||
|
||||
async readPacket(reader: OggReader, startPage: Page, startSegmentIndex: number): Promise<Packet | null> {
|
||||
async readPacket(startPage: Page, startSegmentIndex: number): Promise<Packet | null> {
|
||||
assert(startSegmentIndex < startPage.lacingValues.length);
|
||||
assert(this.fileSize);
|
||||
|
||||
@@ -271,12 +265,10 @@ export class OggDemuxer extends Demuxer {
|
||||
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);
|
||||
let pageSlice = this.reader.requestSlice(currentPage.dataStartPos, currentPage.dataSize);
|
||||
if (pageSlice instanceof Promise) pageSlice = await pageSlice;
|
||||
assert(pageSlice);
|
||||
const pageData = readBytes(pageSlice, currentPage.dataSize);
|
||||
|
||||
while (true) {
|
||||
if (currentSegmentIndex === currentPage.lacingValues.length) {
|
||||
@@ -296,14 +288,19 @@ export class OggDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
// The packet extends to the next page; let's find it
|
||||
let currentPos = currentPage.headerStartPos + currentPage.totalSize;
|
||||
while (true) {
|
||||
reader.pos = currentPage.headerStartPos + currentPage.totalSize;
|
||||
if (reader.pos >= this.fileSize - MIN_PAGE_HEADER_SIZE) {
|
||||
if (currentPos > this.fileSize - MIN_PAGE_HEADER_SIZE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await reader.reader.loadRange(reader.pos, reader.pos + MAX_PAGE_HEADER_SIZE);
|
||||
const nextPage = reader.readPageHeader();
|
||||
let headerSlice = this.reader.requestSliceRange(currentPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE);
|
||||
if (headerSlice instanceof Promise) headerSlice = await headerSlice;
|
||||
if (!headerSlice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextPage = readPageHeader(headerSlice);
|
||||
if (!nextPage) {
|
||||
return null;
|
||||
}
|
||||
@@ -312,6 +309,7 @@ export class OggDemuxer extends Demuxer {
|
||||
if (currentPage.serialNumber === startPage.serialNumber) {
|
||||
break;
|
||||
}
|
||||
currentPos = currentPage.headerStartPos + currentPage.totalSize;
|
||||
}
|
||||
|
||||
startDataOffset = 0;
|
||||
@@ -336,7 +334,7 @@ export class OggDemuxer extends Demuxer {
|
||||
};
|
||||
}
|
||||
|
||||
async findNextPacketStart(reader: OggReader, lastPacket: Packet) {
|
||||
async findNextPacketStart(lastPacket: Packet) {
|
||||
assert(this.fileSize !== null);
|
||||
|
||||
// If there's another segment in the same page, return it
|
||||
@@ -351,14 +349,19 @@ export class OggDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
// Otherwise, search for the next page belonging to the same bitstream
|
||||
reader.pos = lastPacket.endPage.headerStartPos + lastPacket.endPage.totalSize;
|
||||
let currentPos = lastPacket.endPage.headerStartPos + lastPacket.endPage.totalSize;
|
||||
while (true) {
|
||||
if (reader.pos >= this.fileSize - MIN_PAGE_HEADER_SIZE) {
|
||||
if (currentPos >= this.fileSize - MIN_PAGE_HEADER_SIZE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await reader.reader.loadRange(reader.pos, reader.pos + MAX_PAGE_HEADER_SIZE);
|
||||
const nextPage = reader.readPageHeader();
|
||||
let slice = this.reader.requestSliceRange(currentPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
if (!slice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextPage = readPageHeader(slice);
|
||||
if (!nextPage) {
|
||||
return null;
|
||||
}
|
||||
@@ -367,7 +370,7 @@ export class OggDemuxer extends Demuxer {
|
||||
return { startPage: nextPage, startSegmentIndex: 0 };
|
||||
}
|
||||
|
||||
reader.pos = nextPage.headerStartPos + nextPage.totalSize;
|
||||
currentPos = nextPage.headerStartPos + nextPage.totalSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,409 +511,388 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
||||
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 getFirstPacket(options: PacketRetrievalOptions) {
|
||||
assert(this.bitstream.lastMetadataPacket);
|
||||
const packetPosition = await this.demuxer.findNextPacketStart(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(packetPosition.startPage, packetPosition.startSegmentIndex);
|
||||
|
||||
return this.createEncodedPacketFromOggPacket(
|
||||
packet,
|
||||
{
|
||||
timestampInSamples,
|
||||
vorbisLastBlocksize: null,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
const prevMetadata = this.encodedPacketToMetadata.get(prevPacket);
|
||||
if (!prevMetadata) {
|
||||
throw new Error('Packet was not created from this track.');
|
||||
}
|
||||
|
||||
const packetPosition = await this.demuxer.findNextPacketStart(prevMetadata.packet);
|
||||
if (!packetPosition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timestampInSamples = prevMetadata.timestampInSamples + prevMetadata.durationInSamples;
|
||||
|
||||
const packet = await this.demuxer.readPacket(
|
||||
packetPosition.startPage,
|
||||
packetPosition.startSegmentIndex,
|
||||
);
|
||||
|
||||
return this.createEncodedPacketFromOggPacket(
|
||||
packet,
|
||||
{
|
||||
timestampInSamples,
|
||||
vorbisLastBlocksize: prevMetadata.vorbisBlockSize,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
async getPacket(timestamp: number, options: PacketRetrievalOptions) {
|
||||
const release = await this.demuxer.readingMutex.acquire();
|
||||
assert(this.demuxer.fileSize !== null);
|
||||
|
||||
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);
|
||||
}
|
||||
if (timestampInSamples < 0) {
|
||||
// There's nothing here
|
||||
return 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;
|
||||
}
|
||||
assert(this.bitstream.lastMetadataPacket);
|
||||
const startPosition = await this.demuxer.findNextPacketStart(this.bitstream.lastMetadataPacket);
|
||||
if (!startPosition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reader = this.demuxer.reader;
|
||||
let lowPage = startPosition.startPage;
|
||||
let high = this.demuxer.fileSize;
|
||||
|
||||
assert(this.bitstream.lastMetadataPacket);
|
||||
const startPosition = await this.demuxer.findNextPacketStart(
|
||||
reader,
|
||||
this.bitstream.lastMetadataPacket,
|
||||
);
|
||||
if (!startPosition) {
|
||||
return null;
|
||||
}
|
||||
const lowPages: Page[] = [lowPage];
|
||||
|
||||
let lowPage = startPosition.startPage;
|
||||
let high = this.demuxer.fileSize;
|
||||
// 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.
|
||||
|
||||
const lowPages: Page[] = [lowPage];
|
||||
// Outer loop: Does the binary serach
|
||||
outer:
|
||||
while (lowPage.headerStartPos + lowPage.totalSize < high) {
|
||||
const low = lowPage.headerStartPos;
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
|
||||
// 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.
|
||||
let searchStartPos = mid;
|
||||
|
||||
// Outer loop: Does the binary serach
|
||||
outer:
|
||||
while (lowPage.headerStartPos + lowPage.totalSize < high) {
|
||||
const low = lowPage.headerStartPos;
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
// 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,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
let searchSlice = this.demuxer.reader.requestSlice(searchStartPos, until - searchStartPos);
|
||||
if (searchSlice instanceof Promise) searchSlice = await searchSlice;
|
||||
assert(searchSlice);
|
||||
|
||||
const found = findNextPageHeader(searchSlice, until);
|
||||
if (!found) {
|
||||
high = mid + MIN_PAGE_HEADER_SIZE;
|
||||
continue outer;
|
||||
}
|
||||
|
||||
let headerSlice = this.demuxer.reader.requestSliceRange(
|
||||
searchSlice.filePos,
|
||||
MIN_PAGE_HEADER_SIZE,
|
||||
MAX_PAGE_HEADER_SIZE,
|
||||
);
|
||||
if (headerSlice instanceof Promise) headerSlice = await headerSlice;
|
||||
assert(headerSlice);
|
||||
|
||||
const page = readPageHeader(headerSlice);
|
||||
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 {
|
||||
let pageSlice = this.demuxer.reader.requestSlice(page.headerStartPos, page.totalSize);
|
||||
if (pageSlice instanceof Promise) pageSlice = await pageSlice;
|
||||
assert(pageSlice);
|
||||
|
||||
// Validate the page by checking checksum
|
||||
const bytes = readBytes(pageSlice, 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;
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (!lowerPage || otherLowPage.headerStartPos > lowerPage.headerStartPos) {
|
||||
lowerPage = otherLowPage;
|
||||
}
|
||||
}
|
||||
|
||||
let lowerPage = startPosition.startPage;
|
||||
for (const otherLowPage of lowPages) {
|
||||
if (otherLowPage.granulePosition === lowPage.granulePosition) {
|
||||
let currentPage = 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;
|
||||
}
|
||||
|
||||
const nextPos = currentPage.headerStartPos + currentPage.totalSize;
|
||||
let slice = this.demuxer.reader.requestSliceRange(nextPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE);
|
||||
if (slice instanceof Promise) slice = await slice;
|
||||
assert(slice);
|
||||
|
||||
const nextPage = readPageHeader(slice);
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// 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.');
|
||||
}
|
||||
|
||||
assert(currentPage.granulePosition !== -1);
|
||||
endSegmentIndex = currentSegmentIndex - 1;
|
||||
const pseudopacket: Packet = {
|
||||
data: PLACEHOLDER_DATA,
|
||||
endPage,
|
||||
endSegmentIndex,
|
||||
};
|
||||
const nextPosition = await this.demuxer.findNextPacketStart(pseudopacket);
|
||||
|
||||
let currentSegmentIndex: number | null = null;
|
||||
let currentTimestampInSamples: number;
|
||||
let currentTimestampIsCorrect: boolean;
|
||||
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);
|
||||
|
||||
// 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;
|
||||
const startPosition = findPacketStartPosition(
|
||||
previousPages, endPosition.page, endPosition.segmentIndex,
|
||||
);
|
||||
if (startPosition) {
|
||||
currentPage = startPosition.page;
|
||||
currentSegmentIndex = startPosition.segmentIndex;
|
||||
}
|
||||
} 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;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
if (!startPosition) {
|
||||
break;
|
||||
}
|
||||
} 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;
|
||||
|
||||
currentPage = startPosition.page;
|
||||
currentSegmentIndex = startPosition.segmentIndex;
|
||||
|
||||
if (endPosition.page.headerStartPos !== endPage.headerStartPos) {
|
||||
endPage = endPosition.page;
|
||||
endSegmentIndex = endPosition.segmentIndex;
|
||||
break;
|
||||
}
|
||||
if (endPosition.page.headerStartPos !== endPage.headerStartPos) {
|
||||
endPage = endPosition.page;
|
||||
endSegmentIndex = endPosition.segmentIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let lastEncodedPacket: EncodedPacket | null = null;
|
||||
let lastEncodedPacketMetadata: EncodedPacketMetadata | null = null;
|
||||
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);
|
||||
// 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;
|
||||
}
|
||||
const packet = await this.demuxer.readPacket(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;
|
||||
// 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(
|
||||
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,
|
||||
timestampInSamples: currentTimestampInSamples - encodedPacketMetadata.durationInSamples,
|
||||
vorbisLastBlocksize: lastEncodedPacketMetadata?.vorbisBlockSize ?? null,
|
||||
},
|
||||
options,
|
||||
);
|
||||
assert(encodedPacket);
|
||||
|
||||
let encodedPacketMetadata = this.encodedPacketToMetadata.get(encodedPacket);
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
currentTimestampInSamples += encodedPacketMetadata.durationInSamples;
|
||||
}
|
||||
|
||||
const nextPosition = await this.demuxer.findNextPacketStart(reader, packet);
|
||||
if (!nextPosition) {
|
||||
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;
|
||||
}
|
||||
|
||||
currentPage = nextPosition.startPage;
|
||||
currentSegmentIndex = nextPosition.startSegmentIndex;
|
||||
}
|
||||
|
||||
return lastEncodedPacket;
|
||||
} finally {
|
||||
release();
|
||||
const nextPosition = await this.demuxer.findNextPacketStart(packet);
|
||||
if (!nextPosition) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentPage = nextPosition.startPage;
|
||||
currentSegmentIndex = nextPosition.startSegmentIndex;
|
||||
}
|
||||
|
||||
return lastEncodedPacket;
|
||||
}
|
||||
|
||||
getKeyPacket(timestamp: number, options: PacketRetrievalOptions) {
|
||||
|
||||
+50
-100
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Reader } from '../reader';
|
||||
import { FileSlice, readI64Le, readU32Le, readU8 } from '../reader2';
|
||||
import { OGGS } from './ogg-misc';
|
||||
|
||||
export const MIN_PAGE_HEADER_SIZE = 27;
|
||||
@@ -26,118 +26,68 @@ export type Page = {
|
||||
lacingValues: Uint8Array;
|
||||
};
|
||||
|
||||
export class OggReader {
|
||||
pos = 0;
|
||||
constructor(public reader: Reader) {}
|
||||
export const readPageHeader = (slice: FileSlice): Page | null => {
|
||||
const startPos = slice.filePos;
|
||||
|
||||
readBytes(length: number) {
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length);
|
||||
this.pos += length;
|
||||
|
||||
return new Uint8Array(view.buffer, offset, length);
|
||||
const capturePattern = readU32Le(slice);
|
||||
if (capturePattern !== OGGS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
readU8() {
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1);
|
||||
this.pos += 1;
|
||||
slice.skip(1); // Version
|
||||
const headerType = readU8(slice);
|
||||
const granulePosition = readI64Le(slice);
|
||||
const serialNumber = readU32Le(slice);
|
||||
const sequenceNumber = readU32Le(slice);
|
||||
const checksum = readU32Le(slice);
|
||||
|
||||
return view.getUint8(offset);
|
||||
const numberPageSegments = readU8(slice);
|
||||
const lacingValues = new Uint8Array(numberPageSegments);
|
||||
|
||||
for (let i = 0; i < numberPageSegments; i++) {
|
||||
lacingValues[i] = readU8(slice);
|
||||
}
|
||||
|
||||
readU32() {
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4);
|
||||
this.pos += 4;
|
||||
const headerSize = 27 + numberPageSegments;
|
||||
const dataSize = lacingValues.reduce((a, b) => a + b, 0);
|
||||
const totalSize = headerSize + dataSize;
|
||||
|
||||
return view.getUint32(offset, true);
|
||||
}
|
||||
return {
|
||||
headerStartPos: startPos,
|
||||
totalSize,
|
||||
dataStartPos: startPos + headerSize,
|
||||
dataSize,
|
||||
headerType,
|
||||
granulePosition,
|
||||
serialNumber,
|
||||
sequenceNumber,
|
||||
checksum,
|
||||
lacingValues,
|
||||
};
|
||||
};
|
||||
|
||||
readI32() {
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4);
|
||||
this.pos += 4;
|
||||
export const findNextPageHeader = (slice: FileSlice, until: number) => {
|
||||
while (slice.filePos < until - (4 - 1)) { // Size of word minus 1
|
||||
const word = readU32Le(slice);
|
||||
const firstByte = word & 0xff;
|
||||
const secondByte = (word >>> 8) & 0xff;
|
||||
const thirdByte = (word >>> 16) & 0xff;
|
||||
const fourthByte = (word >>> 24) & 0xff;
|
||||
|
||||
return view.getInt32(offset, true);
|
||||
}
|
||||
|
||||
readI64() {
|
||||
const low = this.readU32();
|
||||
const high = this.readI32();
|
||||
return high * 0x100000000 + low;
|
||||
}
|
||||
|
||||
readAscii(length: number) {
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length);
|
||||
this.pos += length;
|
||||
|
||||
let str = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
str += String.fromCharCode(view.getUint8(offset + i));
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
readPageHeader(): Page | null {
|
||||
const startPos = this.pos;
|
||||
|
||||
const capturePattern = this.readU32();
|
||||
if (capturePattern !== OGGS) {
|
||||
return null;
|
||||
const O = 0x4f; // 'O'
|
||||
if (firstByte !== O && secondByte !== O && thirdByte !== O && fourthByte !== O) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.pos += 1; // Version
|
||||
const headerType = this.readU8();
|
||||
const granulePosition = this.readI64();
|
||||
const serialNumber = this.readU32();
|
||||
const sequenceNumber = this.readU32();
|
||||
const checksum = this.readU32();
|
||||
slice.skip(-4);
|
||||
|
||||
const numberPageSegments = this.readU8();
|
||||
const lacingValues = new Uint8Array(numberPageSegments);
|
||||
|
||||
for (let i = 0; i < numberPageSegments; i++) {
|
||||
lacingValues[i] = this.readU8();
|
||||
if (word === OGGS) {
|
||||
// We have found the capture pattern
|
||||
return true;
|
||||
}
|
||||
|
||||
const headerSize = 27 + numberPageSegments;
|
||||
const dataSize = lacingValues.reduce((a, b) => a + b, 0);
|
||||
const totalSize = headerSize + dataSize;
|
||||
|
||||
return {
|
||||
headerStartPos: startPos,
|
||||
totalSize,
|
||||
dataStartPos: startPos + headerSize,
|
||||
dataSize,
|
||||
headerType,
|
||||
granulePosition,
|
||||
serialNumber,
|
||||
sequenceNumber,
|
||||
checksum,
|
||||
lacingValues,
|
||||
};
|
||||
slice.skip(1);
|
||||
}
|
||||
|
||||
findNextPageHeader(until: number) {
|
||||
while (this.pos < until - (4 - 1)) { // Size of word minus 1
|
||||
const word = this.readU32();
|
||||
const firstByte = word & 0xff;
|
||||
const secondByte = (word >>> 8) & 0xff;
|
||||
const thirdByte = (word >>> 16) & 0xff;
|
||||
const fourthByte = (word >>> 24) & 0xff;
|
||||
|
||||
const O = 0x4f; // 'O'
|
||||
if (firstByte !== O && secondByte !== O && thirdByte !== O && fourthByte !== O) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.pos -= 4;
|
||||
|
||||
if (word === OGGS) {
|
||||
// We have found the capture pattern
|
||||
return true;
|
||||
}
|
||||
|
||||
this.pos += 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user