This commit is contained in:
Vanilagy
2025-11-14 14:07:31 +01:00
parent abe6185ddd
commit 7fd2ec6373
11 changed files with 882 additions and 78 deletions
+612
View File
@@ -0,0 +1,612 @@
// Two parallelism modes: Cancel and queue (right?)
// Useful in packet context? Or only for sample?
import { InputTrack, InputVideoTrack } from './input-track';
import { PacketRetrievalOptions, VideoDecoderWrapper } from './media-sink';
import { assert, assertNever, AsyncMutex, AsyncMutex2, insertSorted, last, MaybePromise, promiseWithResolvers, ResultValue, Yo } from './misc';
import { EncodedPacket } from './packet';
import { VideoSample } from './sample';
export class PacketCursor {
track: InputTrack;
_options: PacketRetrievalOptions;
current: EncodedPacket | null = null;
initialized = false;
constructor(track: InputTrack, options: PacketRetrievalOptions = {}) {
this.track = track;
this._options = options;
}
peekAtStart(): MaybePromise<EncodedPacket | null> {
const result = new ResultValue<EncodedPacket | null>();
const promise = this.track._backing.getFirstPacket(result, this._options);
if (result.pending) {
return (promise as Promise<Yo>).then(() => result.value);
} else {
return result.value;
}
}
seekToStart(): MaybePromise<EncodedPacket | null> {
const result = new ResultValue<EncodedPacket | null>();
const promise = this.track._backing.getFirstPacket(result, this._options);
if (result.pending) {
return (promise as Promise<Yo>).then(() => {
this.initialized = true;
return this.current = result.value;
});
} else {
this.initialized = true;
return this.current = result.value;
}
}
peekAt(timestamp: number): MaybePromise<EncodedPacket | null> {
const result = new ResultValue<EncodedPacket | null>();
const promise = this.track._backing.getPacket(result, timestamp, this._options);
if (result.pending) {
return (promise as Promise<Yo>).then(() => result.value);
} else {
return result.value;
}
}
seekTo(timestamp: number): MaybePromise<EncodedPacket | null> {
const result = new ResultValue<EncodedPacket | null>();
const promise = this.track._backing.getPacket(result, timestamp, this._options);
if (result.pending) {
return (promise as Promise<Yo>).then(() => {
this.initialized = true;
return this.current = result.value;
});
} else {
this.initialized = true;
return this.current = result.value;
}
}
peekKeyAt(timestamp: number): MaybePromise<EncodedPacket | null> {
const result = new ResultValue<EncodedPacket | null>();
const promise = this.track._backing.getKeyPacket(result, timestamp, this._options);
if (result.pending) {
return (promise as Promise<Yo>).then(() => result.value);
} else {
return result.value;
}
}
seekToKey(timestamp: number): MaybePromise<EncodedPacket | null> {
const result = new ResultValue<EncodedPacket | null>();
const promise = this.track._backing.getKeyPacket(result, timestamp, this._options);
if (result.pending) {
return (promise as Promise<Yo>).then(() => {
this.initialized = true;
return this.current = result.value;
});
} else {
this.initialized = true;
return this.current = result.value;
}
}
_ensureInitialized() {
if (!this.initialized) {
throw new Error('You must first initialize the cursor to a position by calling any of the seek methods.');
}
}
next(): MaybePromise<EncodedPacket | null> {
this._ensureInitialized();
if (!this.current) {
return null;
}
const result = new ResultValue<EncodedPacket | null>();
const promise = this.track._backing.getNextPacket(result, this.current, this._options);
if (result.pending) {
return (promise as Promise<Yo>).then(() => {
return this.current = result.value;
});
} else {
return this.current = result.value;
}
}
peekNextKey(): MaybePromise<EncodedPacket | null> {
this._ensureInitialized();
if (!this.current) {
return null;
}
const result = new ResultValue<EncodedPacket | null>();
const promise = this.track._backing.getNextKeyPacket(result, this.current, this._options);
if (result.pending) {
return (promise as Promise<Yo>).then(() => result.value);
} else {
return result.value;
}
}
nextKey(): MaybePromise<EncodedPacket | null> {
this._ensureInitialized();
if (!this.current) {
return null;
}
const result = new ResultValue<EncodedPacket | null>();
const promise = this.track._backing.getNextKeyPacket(result, this.current, this._options);
if (result.pending) {
return (promise as Promise<Yo>).then(() => {
return this.current = result.value;
});
} else {
return this.current = result.value;
}
}
async iterate(callback: (packet: EncodedPacket, stop: () => void) => MaybePromise<void>) {
this._ensureInitialized();
let stopped = false;
const stop = () => stopped = true;
while (this.current) {
const result = callback(this.current, stop);
if (result instanceof Promise) await result;
if (stopped) {
break;
}
let next = this.next();
if (next instanceof Promise) next = await next;
this.current = next;
}
}
// eslint-disable-next-line @stylistic/generator-star-spacing
async *[Symbol.asyncIterator]() {
this._ensureInitialized();
while (this.current) {
yield this.current;
let next = this.next();
if (next instanceof Promise) next = await next;
this.current = next;
}
}
}
export class VideoSampleCursor2 {
track: InputVideoTrack;
initialized = false;
packetCursor: PacketCursor;
pumpRunning = false;
decoder: VideoDecoderWrapper;
// current: VideoSample | null = null;
sampleQueue: VideoSample[] = [];
queueDequeue = promiseWithResolvers();
pendingRequests: {
timestamp: number;
resolve: (sample: VideoSample | null) => void;
}[] = [];
stopPump = false;
pumpStopped = promiseWithResolvers();
decodedTimestamps: number[] = [];
maxDecodedSequenceNumber = -1;
pumpMutex = new AsyncMutex2();
private constructor(track: InputVideoTrack, decoder: VideoDecoderWrapper) {
this.track = track;
this.decoder = decoder;
this.packetCursor = new PacketCursor(track);
}
static async init(track: InputVideoTrack) {
if (!(await track.canDecode())) {
throw new Error(
'This video track cannot be decoded by this browser. Make sure to check decodability before using'
+ ' a track.',
);
}
const decoderConfig = await track.getDecoderConfig();
assert(decoderConfig);
assert(track.codec);
const decoder = new VideoDecoderWrapper(
(sample) => {
while (cursor.decodedTimestamps.length > 0 && cursor.decodedTimestamps[0]! <= sample.timestamp) {
cursor.decodedTimestamps.shift();
}
if (cursor.stopPump) {
sample.close();
return;
}
console.log('revc', sample.timestamp);
if (cursor.pendingRequests.length === 0) {
cursor.sampleQueue.push(sample);
} else {
for (let i = 0; i < cursor.pendingRequests.length; i++) {
const request = cursor.pendingRequests[i]!;
if (request.timestamp <= sample.timestamp) {
request.resolve(sample.clone());
cursor.pendingRequests.splice(i--, 1);
}
}
}
// cursor.current?.close();
// cursor.current = sample;
cursor.queueDequeue.resolve();
cursor.queueDequeue = promiseWithResolvers();
},
(error) => {
console.error(error);
},
track.codec,
decoderConfig,
track.rotation,
track.timeResolution,
);
const cursor = new VideoSampleCursor2(track, decoder);
return cursor;
}
getNextExpectedTimestamp() {
if (this.sampleQueue.length > 0) {
return this.sampleQueue[0]!.timestamp;
}
}
async seekTo(timestamp: number): Promise<VideoSample | null> {
this.initialized = true; // too late?
console.log('a');
while (this.pumpMutex.locked) {
console.log('waiting...');
await this.pumpMutex.promise;
}
console.log('GOIN IN');
using _ = this.pumpMutex.lock();
const targetPacket = await this.packetCursor.peekAt(timestamp);
if (!targetPacket) {
return null;
}
let setNewPump = true;
if (this.sampleQueue.length > 0 && targetPacket.timestamp <= this.sampleQueue[0]!.timestamp) {
console.log('This bitch case kicked');
} else {
while (this.sampleQueue.length > 0) {
const nextSample = this.sampleQueue[0]!;
if (targetPacket.timestamp <= nextSample.timestamp) {
console.log('used this path');
return nextSample;
}
this.sampleQueue.shift();
this.queueDequeue.resolve();
this.queueDequeue = promiseWithResolvers();
}
if (this.maxDecodedSequenceNumber !== -1) {
// This means a packet was queued for decode and the cursor is initialized
if (targetPacket.sequenceNumber <= this.maxDecodedSequenceNumber) {
const nextExpectedTimestamp = this.decodedTimestamps[0];
if (!nextExpectedTimestamp || nextExpectedTimestamp > timestamp) {
// yeah
} else {
setNewPump = false;
}
} else {
const key = await this.packetCursor.peekNextKey();
if (!key || targetPacket.sequenceNumber < key.sequenceNumber) {
setNewPump = false;
}
}
}
}
if (setNewPump) {
console.log('setting up a new PUMP');
if (this.pumpRunning) {
this.stopPump = true;
this.queueDequeue.resolve();
this.queueDequeue = promiseWithResolvers();
await this.pumpStopped.promise;
for (const sample of this.sampleQueue) {
sample.close();
}
this.sampleQueue.length = 0;
this.maxDecodedSequenceNumber = -1;
this.decodedTimestamps.length = 0;
this.stopPump = false;
}
await this.packetCursor.seekToKey(timestamp);
void this.runPump();
await Promise.resolve(); // lol
}
const request = promiseWithResolvers<VideoSample | null>();
this.pendingRequests.push({
timestamp: targetPacket.timestamp,
resolve: request.resolve,
});
return request.promise;
}
async next() {
while (this.pumpMutex.locked) {
console.log('waiting next...');
await this.pumpMutex.promise;
}
if (!this.initialized) {
throw new Error('This shud be the indicator the next not being available I think');
}
if (this.sampleQueue.length > 0) {
const nextSample = this.sampleQueue.shift()!;
this.queueDequeue.resolve();
this.queueDequeue = promiseWithResolvers();
return nextSample;
}
if (!this.pumpRunning) {
return null; // None more after this, boy
}
const request = promiseWithResolvers<VideoSample | null>();
this.pendingRequests.push({
timestamp: -Infinity, // Matches any sample timestamp, so any next one will match
resolve: request.resolve,
});
return request.promise;
}
async runPump() {
assert(this.packetCursor.current);
this.pumpRunning = true;
while (this.packetCursor.current && !this.stopPump) {
const maxQueueSize = 8 ?? computeMaxQueueSize(this.sampleQueue.length); // temp
if (this.sampleQueue.length + this.decoder.getDecodeQueueSize() > maxQueueSize) {
await this.queueDequeue.promise;
continue;
}
insertSorted(this.decodedTimestamps, this.packetCursor.current.timestamp, x => x);
this.maxDecodedSequenceNumber = this.packetCursor.current.sequenceNumber;
this.decoder.decode(this.packetCursor.current);
await this.packetCursor.next();
}
console.log('stopping current pump...');
await this.decoder.flush();
this.pumpStopped.resolve();
this.pumpStopped = promiseWithResolvers();
this.pumpRunning = false;
this.pendingRequests.forEach(x => x.resolve(null));
this.pendingRequests.length = 0;
}
}
async function weJustTesting() {
const cursor = new VideoSampleCursor2();
// Spins up decoder and resolves to the sample
await cursor.seekTo(2);
// This can do multiple things:
// - It pops its internal sample queue until it finds a matching frame; in this case, it returns instantly (no promise)
// - If that wasn't possible, but the packet that corresponds to the requested sample was already queued for encoding,
// it will wait for the decoder to spit it out and then returns it. I guess this requires a "pending requests" ahh
// structure somewhere.
// - If that's also not the case, but the seeked packet is in the current GOP, then it just keeps pumping packets into
// the decoder.
// - If the requested packet is outside of the current GOP or "backwards" from the current stream, it resets the
// internal decoder
// In any case, there's always a "pump" running that supplies the decoder with new packets to decode. This pump is
// halted if the internal queue is sufficiently large, and is resumed when samples are consumed.
// This pump is reset if necessary.
await cursor.seekTo(2.1);
}
export class VideoSampleCursor {
track: InputVideoTrack;
current: EncodedPacket | null = null;
initialized = false;
packetCursor: PacketCursor;
packetCursor2: PacketCursor;
decoder!: VideoDecoderWrapper;
constructor(track: InputVideoTrack) {
this.track = track;
this.packetCursor = new PacketCursor(track, { verifyKeyPackets: true });
this.packetCursor2 = new PacketCursor(track, { verifyKeyPackets: true }); // not good
}
async init() {
if (!(await this.track.canDecode())) {
throw new Error(
'This video track cannot be decoded by this browser. Make sure to check decodability before using'
+ ' a track.',
);
}
const decoderConfig = await this.track.getDecoderConfig();
this.decoder = new VideoDecoderWrapper(
(sample) => {
this.sampleQueue.push(sample);
},
(error) => {
// Un que?
},
this.track.codec!,
decoderConfig!,
this.track.rotation,
this.track.timeResolution,
);
}
pumpFinished = promiseWithResolvers();
queueDequeue = promiseWithResolvers();
terminatePump = false;
pumpRunning = false;
sampleQueue: VideoSample[] = [];
async runPump() {
this.pumpRunning = true;
while (this.packetCursor.current && !this.terminatePump) {
const maxQueueSize = computeMaxQueueSize(0);
if (0 + this.decoder.getDecodeQueueSize() > maxQueueSize) {
this.queueDequeue = promiseWithResolvers();
await this.queueDequeue.promise;
continue;
}
this.decoder.decode(this.packetCursor.current);
const result = this.packetCursor.next();
if (result instanceof Promise) await result;
}
await this.decoder.flush();
this.pumpFinished.resolve();
this.pumpRunning = false;
}
async beginNewRun() {
if (this.pumpRunning) {
this.terminatePump = true;
await this.pumpFinished.promise;
for (const sample of this.sampleQueue) {
sample.close();
}
this.sampleQueue.length = 0;
}
// todo errors
void this.runPump();
}
async _seekToCurrentPacket(res: ResultValue<VideoSample | null>): Promise<Yo> {
const targetPacket = this.packetCursor.current;
assert(targetPacket);
if (targetPacket.type !== 'key') {
await this.packetCursor.seekToKey(targetPacket.timestamp);
}
}
/*
seekToStart(): MaybePromise<VideoSample | null> {
const onPacket = (packet: EncodedPacket | null) => {
if (!packet) {
return null;
}
const result = new ResultValue<VideoSample | null>();
const promise = this._seekToPacket(result, packet);
if (result.pending) {
return promise.then(() => result.value);
} else {
return result.value;
}
};
const packet = this.packetCursor.seekToStart();
if (packet instanceof Promise) {
return packet.then(onPacket);
} else {
return onPacket(packet);
}
}
*/
async seekTo(timestamp: number): Promise<VideoSample | null> {
const packet = await this.packetCursor2.seekTo(timestamp);
if (!packet) {
return null; // I guess?
}
if (this.packetCursor.current) {
// if (packet.sequenceNumber)
}
/*
const onPacket = (packet: EncodedPacket | null) => {
if (!packet) {
return null;
}
const result = new ResultValue<VideoSample | null>();
const promise = this._seekToPacket(result, packet);
if (result.pending) {
return promise.then(() => result.value);
} else {
return result.value;
}
};
const packet = this.packetCursor.seekTo(timestamp);
if (packet instanceof Promise) {
return packet.then(onPacket);
} else {
return onPacket(packet);
}
*/
}
}
const computeMaxQueueSize = (decodedSampleQueueSize: number) => {
// If we have decoded samples lying around, limit the total queue size to a small value (decoded samples can use up
// a lot of memory). If not, we're fine with a much bigger queue of encoded packets waiting to be decoded. In fact,
// some decoders only start flushing out decoded chunks when the packet queue is large enough.
return decodedSampleQueueSize === 0 ? 40 : 8;
};
+4
View File
@@ -182,6 +182,10 @@ export {
WrappedAudioBuffer,
WrappedCanvas,
} from './media-sink';
export {
PacketCursor,
VideoSampleCursor2,
} from './cursors';
export {
Conversion,
ConversionOptions,
+1
View File
@@ -259,6 +259,7 @@ export class WebMInputFormat extends MatroskaInputFormat {
export class Mp3InputFormat extends InputFormat {
/** @internal */
async _canReadInput(input: Input) {
return true;
let slice = input._reader.requestSlice(0, 10);
if (slice instanceof Promise) slice = await slice;
if (!slice) return false;
+12 -6
View File
@@ -11,7 +11,7 @@ import { determineVideoPacketType } from './codec-data';
import { customAudioDecoders, customVideoDecoders } from './custom-coder';
import { Input } from './input';
import { EncodedPacketSink, PacketRetrievalOptions } from './media-sink';
import { assert, Rotation } from './misc';
import { assert, MaybePromise, ResultValue, Rotation, Yo } from './misc';
import { TrackType } from './output';
import { EncodedPacket, PacketType } from './packet';
import { TrackDisposition } from './metadata';
@@ -41,11 +41,17 @@ export interface InputTrackBacking {
getFirstTimestamp(): Promise<number>;
computeDuration(): Promise<number>;
getFirstPacket(options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
getPacket(timestamp: number, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
getKeyPacket(timestamp: number, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
getFirstPacket(res: ResultValue<EncodedPacket | null>, options: PacketRetrievalOptions): MaybePromise<Yo>;
getNextPacket(res: ResultValue<EncodedPacket | null>, packet: EncodedPacket, options: PacketRetrievalOptions): MaybePromise<Yo>;
getPacket(res: ResultValue<EncodedPacket | null>, timestamp: number, options: PacketRetrievalOptions): MaybePromise<Yo>;
getKeyPacket(res: ResultValue<EncodedPacket | null>, timestamp: number, options: PacketRetrievalOptions): MaybePromise<Yo>;
getNextKeyPacket(res: ResultValue<EncodedPacket | null>, packet: EncodedPacket, options: PacketRetrievalOptions): MaybePromise<Yo>;
// getFirstPacket(options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
// getPacket(timestamp: number, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
// getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
// getKeyPacket(timestamp: number, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
// getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
}
/**
+78 -34
View File
@@ -58,6 +58,9 @@ import {
UNDETERMINED_LANGUAGE,
toDataView,
roundIfAlmostInteger,
ResultValue,
Yo,
MaybePromise,
} from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { buildIsobmffMimeType } from './isobmff-misc';
@@ -2366,13 +2369,22 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
return firstPacket?.timestamp ?? 0;
}
async getFirstPacket(options: PacketRetrievalOptions) {
const regularPacket = await this.fetchPacketForSampleIndex(0, options);
if (regularPacket || !this.internalTrack.demuxer.isFragmented) {
async getFirstPacket(res: ResultValue<EncodedPacket | null>, options: PacketRetrievalOptions): Promise<Yo> {
const promise = this.fetchPacketForSampleIndex(res, 0, options);
if (res.pending) await promise;
// const regularPacket = res.value;
// const regularPacket = await this.fetchPacketForSampleIndex(0, options);
if (res.value || !this.internalTrack.demuxer.isFragmented) {
// If there's a non-fragmented packet, always prefer that
return regularPacket;
return res.pass();
// return regularPacket;
}
throw new Error('bruh');
/*
return this.performFragmentedLookup(
null,
(fragment) => {
@@ -2393,6 +2405,7 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
Infinity,
options,
);
*/
}
private mapTimestampIntoTimescale(timestamp: number) {
@@ -2402,18 +2415,27 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
return roundIfAlmostInteger(timestamp * this.internalTrack.timescale) + this.internalTrack.editListOffset;
}
async getPacket(timestamp: number, options: PacketRetrievalOptions) {
async getPacket(
res: ResultValue<EncodedPacket | null>,
timestamp: number,
options: PacketRetrievalOptions,
): Promise<Yo> {
const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
const regularPacket = await this.fetchPacketForSampleIndex(sampleIndex, options);
const promise = this.fetchPacketForSampleIndex(res, sampleIndex, options);
if (res.pending) await promise;
if (!sampleTableIsEmpty(sampleTable) || !this.internalTrack.demuxer.isFragmented) {
// Prefer the non-fragmented packet
return regularPacket;
return res.pass();
}
throw new Error('how');
/*
return this.performFragmentedLookup(
null,
(fragment) => {
@@ -2437,16 +2459,24 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
timestampInTimescale,
options,
);
*/
}
async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
const regularSampleIndex = this.packetToSampleIndex.get(packet);
async getNextPacket(
res: ResultValue<EncodedPacket | null>,
packet: EncodedPacket,
options: PacketRetrievalOptions,
): Promise<Yo> {
const regularSampleIndex = packet.sampleIndex;// this.packetToSampleIndex.get(packet);
if (regularSampleIndex !== undefined) {
// Prefer the non-fragmented packet
return this.fetchPacketForSampleIndex(regularSampleIndex + 1, options);
return this.fetchPacketForSampleIndex(res, regularSampleIndex + 1, options);
}
throw new Error('This');
/*
const locationInFragment = this.packetToFragmentLocation.get(packet);
if (locationInFragment === undefined) {
throw new Error('Packet was not created from this track.');
@@ -2483,9 +2513,14 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
Infinity,
options,
);
*/
}
async getKeyPacket(timestamp: number, options: PacketRetrievalOptions) {
async getKeyPacket(
res: ResultValue<EncodedPacket | null>,
timestamp: number,
options: PacketRetrievalOptions,
): Promise<Yo> {
const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
@@ -2493,13 +2528,19 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
const keyFrameSampleIndex = sampleIndex === -1
? -1
: getRelevantKeyframeIndexForSample(sampleTable, sampleIndex);
const regularPacket = await this.fetchPacketForSampleIndex(keyFrameSampleIndex, options);
const promise = this.fetchPacketForSampleIndex(res, keyFrameSampleIndex, options);
if (res.pending) await promise;
if (!sampleTableIsEmpty(sampleTable) || !this.internalTrack.demuxer.isFragmented) {
// Prefer the non-fragmented packet
return regularPacket;
return res.pass();
}
throw new Error('minusch');
/*
return this.performFragmentedLookup(
null,
(fragment) => {
@@ -2522,15 +2563,20 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
timestampInTimescale,
options,
);
*/
}
async getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
const regularSampleIndex = this.packetToSampleIndex.get(packet);
async getNextKeyPacket(
res: ResultValue<EncodedPacket | null>,
packet: EncodedPacket,
options: PacketRetrievalOptions,
): Promise<Yo> {
const regularSampleIndex = packet.sampleIndex;// this.packetToSampleIndex.get(packet);
if (regularSampleIndex !== undefined) {
// Prefer the non-fragmented packet
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, regularSampleIndex);
return this.fetchPacketForSampleIndex(nextKeyFrameSampleIndex, options);
return this.fetchPacketForSampleIndex(res, nextKeyFrameSampleIndex, options);
}
const locationInFragment = this.packetToFragmentLocation.get(packet);
@@ -2538,6 +2584,9 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
throw new Error('Packet was not created from this track.');
}
throw new Error('Sie sagen Mel');
/*
return this.performFragmentedLookup(
locationInFragment.fragment,
(fragment) => {
@@ -2576,17 +2625,22 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
Infinity,
options,
);
*/
}
private async fetchPacketForSampleIndex(sampleIndex: number, options: PacketRetrievalOptions) {
private async fetchPacketForSampleIndex(
res: ResultValue<EncodedPacket | null>,
sampleIndex: number,
options: PacketRetrievalOptions,
): Promise<Yo> {
if (sampleIndex === -1) {
return null;
return res.set(null);
}
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
const sampleInfo = getSampleInfo(sampleTable, sampleIndex);
if (!sampleInfo) {
return null;
return res.set(null);
}
let data: Uint8Array;
@@ -2615,9 +2669,10 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
sampleInfo.sampleSize,
);
this.packetToSampleIndex.set(packet, sampleIndex);
packet.sampleIndex = sampleIndex;
// this.packetToSampleIndex.set(packet, sampleIndex);
return packet;
return res.set(packet);
}
private async fetchPacketInFragment(fragment: Fragment, sampleIndex: number, options: PacketRetrievalOptions) {
@@ -2922,8 +2977,6 @@ type SampleInfo = {
duration: number;
sampleOffset: number;
sampleSize: number;
chunkOffset: number;
chunkSize: number;
isKeyFrame: boolean;
};
@@ -2958,20 +3011,13 @@ const getSampleInfo = (sampleTable: SampleTable, sampleIndex: number): SampleInf
const startSampleIndexOfChunk = chunkEntry.startSampleIndex
+ (chunkIndex - chunkEntry.startChunkIndex) * chunkEntry.samplesPerChunk;
let chunkSize = 0;
let sampleOffset = chunkOffset;
if (sampleTable.sampleSizes.length === 1) {
sampleOffset += sampleSize * (sampleIndex - startSampleIndexOfChunk);
chunkSize += sampleSize * chunkEntry.samplesPerChunk;
} else {
for (let i = startSampleIndexOfChunk; i < startSampleIndexOfChunk + chunkEntry.samplesPerChunk; i++) {
const sampleSize = sampleTable.sampleSizes[i]!;
if (i < sampleIndex) {
sampleOffset += sampleSize;
}
chunkSize += sampleSize;
for (let i = startSampleIndexOfChunk; i < sampleIndex; i++) {
sampleOffset += sampleTable.sampleSizes[i]!;
}
}
@@ -2994,8 +3040,6 @@ const getSampleInfo = (sampleTable: SampleTable, sampleIndex: number): SampleInf
duration,
sampleOffset,
sampleSize,
chunkOffset,
chunkSize,
isKeyFrame: sampleTable.keySampleIndices
? binarySearchExact(sampleTable.keySampleIndices, sampleIndex, x => x) !== -1
: true,
+19 -4
View File
@@ -32,12 +32,15 @@ import {
isWebKit,
last,
mapAsyncGenerator,
MaybePromise,
promiseWithResolvers,
ResultValue,
Rotation,
toAsyncIterator,
toDataView,
toUint8Array,
validateAnyIterable,
Yo,
} from './misc';
import { EncodedPacket } from './packet';
import { fromAlaw, fromUlaw } from './pcm';
@@ -131,14 +134,20 @@ export class EncodedPacketSink {
* Retrieves the track's first packet (in decode order), or null if it has no packets. The first packet is very
* likely to be a key packet.
*/
getFirstPacket(options: PacketRetrievalOptions = {}) {
getFirstPacket(options: PacketRetrievalOptions = {}): MaybePromise<EncodedPacket | null> {
validatePacketRetrievalOptions(options);
if (this._track.input._disposed) {
throw new InputDisposedError();
}
return maybeFixPacketType(this._track, this._track._backing.getFirstPacket(options), options);
const result = new ResultValue<EncodedPacket | null>();
const promise = this._track._backing.getFirstPacket(result, options);
if (result.pending) return (promise as Promise<Yo>).then(() => result.value);
return result.value;
// return maybeFixPacketType(this._track, this._track._backing.getFirstPacket(options), options);
}
/**
@@ -174,7 +183,13 @@ export class EncodedPacketSink {
throw new InputDisposedError();
}
return maybeFixPacketType(this._track, this._track._backing.getNextPacket(packet, options), options);
const result = new ResultValue<EncodedPacket | null>();
const promise = this._track._backing.getNextPacket(result, packet, options);
if (result.pending) return (promise as Promise<Yo>).then(() => result.value);
return result.value;
// return maybeFixPacketType(this._track, this._track._backing.getNextPacket(packet, options), options);
}
/**
@@ -809,7 +824,7 @@ const computeMaxQueueSize = (decodedSampleQueueSize: number) => {
return decodedSampleQueueSize === 0 ? 40 : 8;
};
class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
export class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
decoder: VideoDecoder | null = null;
customDecoder: CustomVideoDecoder | null = null;
+43
View File
@@ -796,3 +796,46 @@ export const polyfillSymbolDispose = () => {
export const isNumber = (x: unknown) => {
return typeof x === 'number' && !Number.isNaN(x);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const yo: unique symbol = Symbol();
export type Yo = typeof yo;
export class ResultValue<T> {
value!: T;
pending = true;
// @ts-expect-error Just for the types
set(value: T): Yo {
this.value = value;
this.pending = false;
}
// @ts-expect-error Just for the types
pass(): Yo {
assert(!this.pending);
}
}
export class AsyncMutex2 {
locked = false;
promise = Promise.resolve();
lock() {
if (this.locked) {
throw new Error('Mutex already locked.');
}
this.locked = true;
const { promise, resolve } = promiseWithResolvers();
this.promise = promise;
return {
[Symbol.dispose]: () => {
resolve();
this.locked = false;
},
};
}
}
+43 -27
View File
@@ -12,7 +12,7 @@ import { Input } from '../input';
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
import { PacketRetrievalOptions } from '../media-sink';
import { assert, AsyncMutex, binarySearchExact, binarySearchLessOrEqual, UNDETERMINED_LANGUAGE } from '../misc';
import { assert, AsyncMutex, AsyncMutex2, binarySearchExact, binarySearchLessOrEqual, ResultValue, UNDETERMINED_LANGUAGE, Yo } from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { FrameHeader, getXingOffset, INFO, XING } from '../../shared/mp3-misc';
import {
@@ -42,7 +42,8 @@ export class Mp3Demuxer extends Demuxer {
tracks: InputAudioTrack[] = [];
readingMutex = new AsyncMutex();
// readingMutex = new AsyncMutex();
readingMutex = new AsyncMutex2();
lastSampleLoaded = false;
lastLoadedPos = 0;
nextTimestampInSamples = 0;
@@ -57,7 +58,9 @@ export class Mp3Demuxer extends Demuxer {
return this.metadataPromise ??= (async () => {
// Keep loading until we find the first frame header
while (!this.firstFrameHeader && !this.lastSampleLoaded) {
await this.advanceReader();
const result = new ResultValue<void>();
const promise = this.advanceReader(result);
if (result.pending) await promise;
}
if (!this.firstFrameHeader) {
@@ -68,7 +71,7 @@ export class Mp3Demuxer extends Demuxer {
})();
}
async advanceReader() {
async advanceReader(res: ResultValue<void>): Promise<Yo> {
if (this.lastLoadedPos === 0) {
// Let's skip all ID3v2 tags at the start of the file
while (true) {
@@ -77,7 +80,7 @@ export class Mp3Demuxer extends Demuxer {
if (!slice) {
this.lastSampleLoaded = true;
return;
return res.set();
}
const id3V2Header = readId3V2Header(slice);
@@ -89,19 +92,25 @@ export class Mp3Demuxer extends Demuxer {
}
}
const result = await readNextFrameHeader(this.reader, this.lastLoadedPos, this.reader.fileSize);
if (!result) {
const result = new ResultValue<{
header: FrameHeader;
startPos: number;
} | null>();
const promise = readNextFrameHeader(result, this.reader, this.lastLoadedPos, this.reader.fileSize);
if (result.pending) await promise;
if (!result.value) {
this.lastSampleLoaded = true;
return;
return res.set();
}
const header = result.header;
const header = result.value.header;
this.lastLoadedPos = result.startPos + header.totalSize - 1; // -1 in case the frame is 1 byte too short
this.lastLoadedPos = result.value.startPos + header.totalSize - 1; // -1 in case the frame is 1 byte too short
const xingOffset = getXingOffset(header.mpegVersionId, header.channel);
let slice = this.reader.requestSlice(result.startPos + xingOffset, 4);
let slice = this.reader.requestSlice(result.value.startPos + xingOffset, 4);
if (slice instanceof Promise) slice = await slice;
if (slice) {
const word = readU32Be(slice);
@@ -109,7 +118,7 @@ export class Mp3Demuxer extends Demuxer {
if (isXing) {
// There's no actual audio data in this frame, so let's skip it
return;
return res.set();
}
}
@@ -128,14 +137,14 @@ export class Mp3Demuxer extends Demuxer {
const sample: Sample = {
timestamp: this.nextTimestampInSamples / this.firstFrameHeader.sampleRate,
duration: sampleDuration,
dataStart: result.startPos,
dataStart: result.value.startPos,
dataSize: header.totalSize,
};
this.loadedSamples.push(sample);
this.nextTimestampInSamples += header.audioSamplesInFrame;
return;
return res.set();
}
async getMimeType() {
@@ -273,14 +282,14 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
};
}
async getPacketAtIndex(sampleIndex: number, options: PacketRetrievalOptions) {
async getPacketAtIndex(res: ResultValue<EncodedPacket | null>, sampleIndex: number, options: PacketRetrievalOptions): Promise<Yo> {
if (sampleIndex === -1) {
return null;
return res.set(null);
}
const rawSample = this.demuxer.loadedSamples[sampleIndex];
if (!rawSample) {
return null;
return res.set(null);
}
let data: Uint8Array;
@@ -291,28 +300,33 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
if (slice instanceof Promise) slice = await slice;
if (!slice) {
return null; // Data didn't fit into the rest of the file
return res.set(null); // Data didn't fit into the rest of the file
}
data = readBytes(slice, rawSample.dataSize);
}
return new EncodedPacket(
return res.set(new EncodedPacket(
data,
'key',
rawSample.timestamp,
rawSample.duration,
sampleIndex,
rawSample.dataSize,
);
));
}
getFirstPacket(options: PacketRetrievalOptions) {
return this.getPacketAtIndex(0, options);
getFirstPacket(res: ResultValue<EncodedPacket | null>, options: PacketRetrievalOptions) {
return this.getPacketAtIndex(res, 0, options);
}
async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
const release = await this.demuxer.readingMutex.acquire();
async getNextPacket(res: ResultValue<EncodedPacket | null>, packet: EncodedPacket, options: PacketRetrievalOptions): Promise<Yo> {
// using foo = 5;
while (this.demuxer.readingMutex.locked) await this.demuxer.readingMutex.promise;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
using _ = this.demuxer.readingMutex.lock();
// const release = await this.demuxer.readingMutex.acquire();
try {
const sampleIndex = binarySearchExact(
@@ -330,12 +344,14 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
nextIndex >= this.demuxer.loadedSamples.length
&& !this.demuxer.lastSampleLoaded
) {
await this.demuxer.advanceReader();
const result = new ResultValue<void>();
const promise = this.demuxer.advanceReader(result);
if (result.pending) await promise;
}
return this.getPacketAtIndex(nextIndex, options);
return this.getPacketAtIndex(res, nextIndex, options);
} finally {
release();
// release();
}
}
+12 -6
View File
@@ -7,12 +7,18 @@
*/
import { FRAME_HEADER_SIZE, FrameHeader, readFrameHeader } from '../../shared/mp3-misc';
import { ResultValue, Yo } from '../misc';
import { Reader, readU32Be } from '../reader';
export const readNextFrameHeader = async (reader: Reader, startPos: number, until: number | null): Promise<{
header: FrameHeader;
startPos: number;
} | null> => {
export const readNextFrameHeader = async (
res: ResultValue<{
header: FrameHeader;
startPos: number;
} | null>,
reader: Reader,
startPos: number,
until: number | null,
): Promise<Yo> => {
let currentPos = startPos;
while (until === null || currentPos < until) {
@@ -24,11 +30,11 @@ export const readNextFrameHeader = async (reader: Reader, startPos: number, unti
const result = readFrameHeader(word, reader.fileSize !== null ? reader.fileSize - currentPos : null);
if (result.header) {
return { header: result.header, startPos: currentPos };
return res.set({ header: result.header, startPos: currentPos });
}
currentPos += result.bytesAdvanced;
}
return null;
return res.set(null);
};