mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-02 21:33:52 +02:00
Fix Ogg demuxer async race conditions
This commit is contained in:
+346
-319
@@ -3,7 +3,7 @@ import { Demuxer } from '../demuxer';
|
|||||||
import { Input } from '../input';
|
import { Input } from '../input';
|
||||||
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
||||||
import { PacketRetrievalOptions } from '../media-sink';
|
import { PacketRetrievalOptions } from '../media-sink';
|
||||||
import { assert, findLast, roundToPrecision, toDataView, UNDETERMINED_LANGUAGE } from '../misc';
|
import { assert, AsyncMutex, findLast, roundToPrecision, toDataView, UNDETERMINED_LANGUAGE } from '../misc';
|
||||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||||
import { Reader } from '../reader';
|
import { Reader } from '../reader';
|
||||||
import { computeOggPageCrc, extractSampleMetadata, OggCodecInfo, parseModesFromVorbisSetupPacket } from './ogg-misc';
|
import { computeOggPageCrc, extractSampleMetadata, OggCodecInfo, parseModesFromVorbisSetupPacket } from './ogg-misc';
|
||||||
@@ -28,6 +28,11 @@ type Packet = {
|
|||||||
|
|
||||||
export class OggDemuxer extends Demuxer {
|
export class OggDemuxer extends Demuxer {
|
||||||
reader: OggReader;
|
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;
|
metadataPromise: Promise<void> | null = null;
|
||||||
fileSize: number | null = null;
|
fileSize: number | null = null;
|
||||||
@@ -491,387 +496,409 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
|||||||
return encodedPacket;
|
return encodedPacket;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFirstPacket(options: PacketRetrievalOptions) {
|
async getFirstPacket(options: PacketRetrievalOptions, exclusive = true) {
|
||||||
assert(this.bitstream.lastMetadataPacket);
|
const release = exclusive ? await this.demuxer.readingMutex.acquire() : null;
|
||||||
const packetPosition = await this.demuxer.findNextPacketStart(
|
|
||||||
this.demuxer.reader,
|
try {
|
||||||
this.bitstream.lastMetadataPacket,
|
assert(this.bitstream.lastMetadataPacket);
|
||||||
);
|
const packetPosition = await this.demuxer.findNextPacketStart(
|
||||||
if (!packetPosition) {
|
this.demuxer.reader,
|
||||||
return null;
|
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?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextPacket(prevPacket: EncodedPacket, options: PacketRetrievalOptions) {
|
async getNextPacket(prevPacket: EncodedPacket, options: PacketRetrievalOptions) {
|
||||||
const prevMetadata = this.encodedPacketToMetadata.get(prevPacket);
|
const release = await this.demuxer.readingMutex.acquire();
|
||||||
if (!prevMetadata) {
|
|
||||||
throw new Error('Packet was not created from this track.');
|
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 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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPacket(timestamp: number, options: PacketRetrievalOptions) {
|
async getPacket(timestamp: number, options: PacketRetrievalOptions) {
|
||||||
assert(this.demuxer.fileSize !== null);
|
const release = await this.demuxer.readingMutex.acquire();
|
||||||
|
|
||||||
const timestampInSamples = roundToPrecision(timestamp * this.internalSampleRate, 14);
|
try {
|
||||||
if (timestampInSamples === 0) {
|
assert(this.demuxer.fileSize !== null);
|
||||||
// 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 reader = this.demuxer.reader;
|
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 reader = this.demuxer.reader;
|
||||||
const startPosition = await this.demuxer.findNextPacketStart(
|
|
||||||
reader,
|
|
||||||
this.bitstream.lastMetadataPacket,
|
|
||||||
);
|
|
||||||
if (!startPosition) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lowPage = startPosition.startPage;
|
assert(this.bitstream.lastMetadataPacket);
|
||||||
let high = this.demuxer.fileSize;
|
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
|
const lowPages: Page[] = [lowPage];
|
||||||
// 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
|
// First, let's perform a binary serach (bisection search) on the file to find the approximate page where
|
||||||
outer:
|
// we'll find the packet. We want to find a page whose end packet position is less than or equal to the
|
||||||
while (lowPage.headerStartPos + lowPage.totalSize < high) {
|
// packet position we're searching for.
|
||||||
const low = lowPage.headerStartPos;
|
|
||||||
const mid = Math.floor((low + high) / 2);
|
|
||||||
|
|
||||||
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
|
let searchStartPos = mid;
|
||||||
while (true) {
|
|
||||||
const until = Math.min(
|
|
||||||
searchStartPos + MAX_PAGE_SIZE,
|
|
||||||
high - MIN_PAGE_HEADER_SIZE,
|
|
||||||
);
|
|
||||||
|
|
||||||
await reader.reader.loadRange(searchStartPos, until);
|
// 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,
|
||||||
|
);
|
||||||
|
|
||||||
reader.pos = searchStartPos;
|
await reader.reader.loadRange(searchStartPos, until);
|
||||||
const found = reader.findNextPageHeader(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);
|
||||||
|
}
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
high = mid + MIN_PAGE_HEADER_SIZE;
|
|
||||||
continue outer;
|
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) {
|
// Now we have the last page with a packet position <= the packet position we're looking for, but there
|
||||||
lowerPage = otherLowPage;
|
// 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 currentPage: Page | null = lowerPage;
|
let lowerPage = startPosition.startPage;
|
||||||
// Keep track of the pages we traversed, we need these later for backwards seeking
|
for (const otherLowPage of lowPages) {
|
||||||
const previousPages: Page[] = [currentPage];
|
if (otherLowPage.granulePosition === lowPage.granulePosition) {
|
||||||
|
|
||||||
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// This must hold: Since this page has a granule position set, that means there must be a packet that ends
|
if (!lowerPage || otherLowPage.headerStartPos > lowerPage.headerStartPos) {
|
||||||
// in this page.
|
lowerPage = otherLowPage;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
// There is no next position, which means we're looking for the last packet in the bitstream. The
|
currentTimestampInSamples = 0; // Placeholder value! We'll refine it once we can
|
||||||
// granule position on the last page tends to be fucky, so let's instead start the search on the page
|
currentTimestampIsCorrect = false;
|
||||||
// before that. So let's loop until we find a packet that ends in a previous page.
|
|
||||||
while (true) {
|
// Find the segment index of the next packet
|
||||||
const endPosition = findPreviousPacketEndPosition(previousPages, currentPage, currentSegmentIndex);
|
for (let i = currentPage.lacingValues.length - 1; i >= 0; i--) {
|
||||||
if (!endPosition) {
|
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;
|
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(
|
const startPosition = findPacketStartPosition(
|
||||||
previousPages, endPosition.page, endPosition.segmentIndex,
|
previousPages, endPosition.page, endPosition.segmentIndex,
|
||||||
);
|
);
|
||||||
if (!startPosition) {
|
if (startPosition) {
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
currentPage = startPosition.page;
|
const startPosition = findPacketStartPosition(
|
||||||
currentSegmentIndex = startPosition.segmentIndex;
|
previousPages, endPosition.page, endPosition.segmentIndex,
|
||||||
|
);
|
||||||
|
if (!startPosition) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (endPosition.page.headerStartPos !== endPage.headerStartPos) {
|
currentPage = startPosition.page;
|
||||||
endPage = endPosition.page;
|
currentSegmentIndex = startPosition.segmentIndex;
|
||||||
endSegmentIndex = endPosition.segmentIndex;
|
|
||||||
break;
|
if (endPosition.page.headerStartPos !== endPage.headerStartPos) {
|
||||||
|
endPage = endPosition.page;
|
||||||
|
endSegmentIndex = endPosition.segmentIndex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let lastEncodedPacket: EncodedPacket | null = null;
|
let lastEncodedPacket: EncodedPacket | null = null;
|
||||||
let lastEncodedPacketMetadata: EncodedPacketMetadata | 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
|
// Alright, now it's time for the final, granular seek: We keep iterating over packets until we've found the
|
||||||
// with the correct timestamp - i.e., the last one with a timestamp <= the timestamp we're looking for.
|
// one with the correct timestamp - i.e., the last one with a timestamp <= the timestamp we're looking for.
|
||||||
while (currentPage !== null) {
|
while (currentPage !== null) {
|
||||||
assert(currentSegmentIndex !== null);
|
assert(currentSegmentIndex !== null);
|
||||||
|
|
||||||
const packet = await this.demuxer.readPacket(reader, currentPage, currentSegmentIndex);
|
const packet = await this.demuxer.readPacket(reader, currentPage, currentSegmentIndex);
|
||||||
if (!packet) {
|
if (!packet) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We might need to skip the packet if it's a metadata one
|
// We might need to skip the packet if it's a metadata one
|
||||||
const skipPacket = currentPage.headerStartPos === startPosition.startPage.headerStartPos
|
const skipPacket = currentPage.headerStartPos === startPosition.startPage.headerStartPos
|
||||||
&& currentSegmentIndex < startPosition.startSegmentIndex;
|
&& currentSegmentIndex < startPosition.startSegmentIndex;
|
||||||
|
|
||||||
if (!skipPacket) {
|
if (!skipPacket) {
|
||||||
let encodedPacket = this.createEncodedPacketFromOggPacket(
|
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,
|
packet,
|
||||||
{
|
{
|
||||||
timestampInSamples: currentTimestampInSamples - encodedPacketMetadata.durationInSamples,
|
timestampInSamples: currentTimestampInSamples,
|
||||||
vorbisLastBlocksize: lastEncodedPacketMetadata?.vorbisBlockSize ?? null,
|
vorbisLastBlocksize: lastEncodedPacketMetadata?.vorbisBlockSize ?? null,
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
assert(encodedPacket);
|
assert(encodedPacket);
|
||||||
|
|
||||||
encodedPacketMetadata = this.encodedPacketToMetadata.get(encodedPacket);
|
let encodedPacketMetadata = this.encodedPacketToMetadata.get(encodedPacket);
|
||||||
assert(encodedPacketMetadata);
|
assert(encodedPacketMetadata);
|
||||||
} else {
|
|
||||||
currentTimestampInSamples += encodedPacketMetadata.durationInSamples;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lastEncodedPacket = encodedPacket;
|
const nextPosition = await this.demuxer.findNextPacketStart(reader, packet);
|
||||||
lastEncodedPacketMetadata = encodedPacketMetadata;
|
if (!nextPosition) {
|
||||||
|
|
||||||
if (
|
|
||||||
currentTimestampIsCorrect
|
|
||||||
&& (
|
|
||||||
// Next timestamp will be too late
|
|
||||||
Math.max(currentTimestampInSamples, 0) > timestampInSamples
|
|
||||||
// This timestamp already matches
|
|
||||||
|| Math.max(encodedPacketMetadata.timestampInSamples, 0) === timestampInSamples
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentPage = nextPosition.startPage;
|
||||||
|
currentSegmentIndex = nextPosition.startSegmentIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPosition = await this.demuxer.findNextPacketStart(reader, packet);
|
return lastEncodedPacket;
|
||||||
if (!nextPosition) {
|
} finally {
|
||||||
break;
|
release();
|
||||||
}
|
|
||||||
|
|
||||||
currentPage = nextPosition.startPage;
|
|
||||||
currentSegmentIndex = nextPosition.startSegmentIndex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return lastEncodedPacket;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getKeyPacket(timestamp: number, options: PacketRetrievalOptions) {
|
getKeyPacket(timestamp: number, options: PacketRetrievalOptions) {
|
||||||
|
|||||||
Reference in New Issue
Block a user