mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 13:53:53 +02:00
WIP
This commit is contained in:
+57
-1
@@ -14,7 +14,61 @@
|
|||||||
source: new Mediabunny.BlobSource(file),
|
source: new Mediabunny.BlobSource(file),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(await input.getTracks());
|
const videoTrack = await input.getPrimaryVideoTrack();
|
||||||
|
const cursor = await Mediabunny.VideoSampleCursor2.init(videoTrack);
|
||||||
|
|
||||||
|
/*
|
||||||
|
console.log(await cursor.seekTo(2.00));
|
||||||
|
console.log(await cursor.next());
|
||||||
|
console.log(await cursor.next());
|
||||||
|
*/
|
||||||
|
|
||||||
|
const mh = [
|
||||||
|
cursor.seekTo(2.10),
|
||||||
|
cursor.seekTo(5),
|
||||||
|
//cursor.seekTo(2.00),
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(await Promise.all(mh));
|
||||||
|
|
||||||
|
//console.log(await cursor.seekTo(2.05));
|
||||||
|
//console.log(await cursor.seekTo(2.10));
|
||||||
|
//console.log(await cursor.seekTo(10.2));
|
||||||
|
//console.log(await cursor.seekTo(5.2));
|
||||||
|
|
||||||
|
/*
|
||||||
|
setTimeout(async () => {
|
||||||
|
console.log("this")
|
||||||
|
|
||||||
|
}, 500);
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
const iters = 5;
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < iters; i++) {
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
const input = new Mediabunny.Input({
|
||||||
|
formats: Mediabunny.ALL_FORMATS,
|
||||||
|
source: new Mediabunny.BufferSource(await file.arrayBuffer()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const audioTrack = await input.getPrimaryAudioTrack();
|
||||||
|
const cursor = new Mediabunny.PacketCursor(audioTrack);
|
||||||
|
|
||||||
|
await cursor.seekToStart();
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
await cursor.iterate(() => {});
|
||||||
|
//alert(performance.now() - start)
|
||||||
|
|
||||||
|
total += performance.now() - start;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(total / iters);
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
return;
|
return;
|
||||||
@@ -95,6 +149,8 @@
|
|||||||
total += end - start;
|
total += end - start;
|
||||||
//console.log(total / file.size, end - start);
|
//console.log(total / file.size, end - start);
|
||||||
}
|
}
|
||||||
|
alert(elapsed / iters);
|
||||||
|
|
||||||
|
|
||||||
const videoTrack = await input.getPrimaryVideoTrack();
|
const videoTrack = await input.getPrimaryVideoTrack();
|
||||||
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
|
const sink = new Mediabunny.EncodedPacketSink(videoTrack);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const createVariants = async (
|
|||||||
entryPoints: [entryPoint],
|
entryPoints: [entryPoint],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
|
target: 'es2024', // Keep? This makes sure "using" is transformed
|
||||||
logOverride: {
|
logOverride: {
|
||||||
'import-is-undefined': 'silent', // Warning caused by the disabled "node.ts" import
|
'import-is-undefined': 'silent', // Warning caused by the disabled "node.ts" import
|
||||||
},
|
},
|
||||||
|
|||||||
+612
@@ -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;
|
||||||
|
};
|
||||||
@@ -182,6 +182,10 @@ export {
|
|||||||
WrappedAudioBuffer,
|
WrappedAudioBuffer,
|
||||||
WrappedCanvas,
|
WrappedCanvas,
|
||||||
} from './media-sink';
|
} from './media-sink';
|
||||||
|
export {
|
||||||
|
PacketCursor,
|
||||||
|
VideoSampleCursor2,
|
||||||
|
} from './cursors';
|
||||||
export {
|
export {
|
||||||
Conversion,
|
Conversion,
|
||||||
ConversionOptions,
|
ConversionOptions,
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ export class WebMInputFormat extends MatroskaInputFormat {
|
|||||||
export class Mp3InputFormat extends InputFormat {
|
export class Mp3InputFormat extends InputFormat {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
async _canReadInput(input: Input) {
|
async _canReadInput(input: Input) {
|
||||||
|
return true;
|
||||||
let slice = input._reader.requestSlice(0, 10);
|
let slice = input._reader.requestSlice(0, 10);
|
||||||
if (slice instanceof Promise) slice = await slice;
|
if (slice instanceof Promise) slice = await slice;
|
||||||
if (!slice) return false;
|
if (!slice) return false;
|
||||||
|
|||||||
+12
-6
@@ -11,7 +11,7 @@ import { determineVideoPacketType } from './codec-data';
|
|||||||
import { customAudioDecoders, customVideoDecoders } from './custom-coder';
|
import { customAudioDecoders, customVideoDecoders } from './custom-coder';
|
||||||
import { Input } from './input';
|
import { Input } from './input';
|
||||||
import { EncodedPacketSink, PacketRetrievalOptions } from './media-sink';
|
import { EncodedPacketSink, PacketRetrievalOptions } from './media-sink';
|
||||||
import { assert, Rotation } from './misc';
|
import { assert, MaybePromise, ResultValue, Rotation, Yo } from './misc';
|
||||||
import { TrackType } from './output';
|
import { TrackType } from './output';
|
||||||
import { EncodedPacket, PacketType } from './packet';
|
import { EncodedPacket, PacketType } from './packet';
|
||||||
import { TrackDisposition } from './metadata';
|
import { TrackDisposition } from './metadata';
|
||||||
@@ -41,11 +41,17 @@ export interface InputTrackBacking {
|
|||||||
getFirstTimestamp(): Promise<number>;
|
getFirstTimestamp(): Promise<number>;
|
||||||
computeDuration(): Promise<number>;
|
computeDuration(): Promise<number>;
|
||||||
|
|
||||||
getFirstPacket(options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
|
getFirstPacket(res: ResultValue<EncodedPacket | null>, options: PacketRetrievalOptions): MaybePromise<Yo>;
|
||||||
getPacket(timestamp: number, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
|
getNextPacket(res: ResultValue<EncodedPacket | null>, packet: EncodedPacket, options: PacketRetrievalOptions): MaybePromise<Yo>;
|
||||||
getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
|
getPacket(res: ResultValue<EncodedPacket | null>, timestamp: number, options: PacketRetrievalOptions): MaybePromise<Yo>;
|
||||||
getKeyPacket(timestamp: number, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
|
getKeyPacket(res: ResultValue<EncodedPacket | null>, timestamp: number, options: PacketRetrievalOptions): MaybePromise<Yo>;
|
||||||
getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions): Promise<EncodedPacket | null>;
|
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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ import {
|
|||||||
UNDETERMINED_LANGUAGE,
|
UNDETERMINED_LANGUAGE,
|
||||||
toDataView,
|
toDataView,
|
||||||
roundIfAlmostInteger,
|
roundIfAlmostInteger,
|
||||||
|
ResultValue,
|
||||||
|
Yo,
|
||||||
|
MaybePromise,
|
||||||
} from '../misc';
|
} from '../misc';
|
||||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||||
import { buildIsobmffMimeType } from './isobmff-misc';
|
import { buildIsobmffMimeType } from './isobmff-misc';
|
||||||
@@ -2366,13 +2369,22 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
return firstPacket?.timestamp ?? 0;
|
return firstPacket?.timestamp ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFirstPacket(options: PacketRetrievalOptions) {
|
async getFirstPacket(res: ResultValue<EncodedPacket | null>, options: PacketRetrievalOptions): Promise<Yo> {
|
||||||
const regularPacket = await this.fetchPacketForSampleIndex(0, options);
|
const promise = this.fetchPacketForSampleIndex(res, 0, options);
|
||||||
if (regularPacket || !this.internalTrack.demuxer.isFragmented) {
|
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
|
// If there's a non-fragmented packet, always prefer that
|
||||||
return regularPacket;
|
return res.pass();
|
||||||
|
// return regularPacket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error('bruh');
|
||||||
|
|
||||||
|
/*
|
||||||
return this.performFragmentedLookup(
|
return this.performFragmentedLookup(
|
||||||
null,
|
null,
|
||||||
(fragment) => {
|
(fragment) => {
|
||||||
@@ -2393,6 +2405,7 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
Infinity,
|
Infinity,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapTimestampIntoTimescale(timestamp: number) {
|
private mapTimestampIntoTimescale(timestamp: number) {
|
||||||
@@ -2402,18 +2415,27 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
return roundIfAlmostInteger(timestamp * this.internalTrack.timescale) + this.internalTrack.editListOffset;
|
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 timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
|
||||||
|
|
||||||
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
||||||
const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
|
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) {
|
if (!sampleTableIsEmpty(sampleTable) || !this.internalTrack.demuxer.isFragmented) {
|
||||||
// Prefer the non-fragmented packet
|
// Prefer the non-fragmented packet
|
||||||
return regularPacket;
|
return res.pass();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error('how');
|
||||||
|
|
||||||
|
/*
|
||||||
return this.performFragmentedLookup(
|
return this.performFragmentedLookup(
|
||||||
null,
|
null,
|
||||||
(fragment) => {
|
(fragment) => {
|
||||||
@@ -2437,16 +2459,24 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
timestampInTimescale,
|
timestampInTimescale,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
|
async getNextPacket(
|
||||||
const regularSampleIndex = this.packetToSampleIndex.get(packet);
|
res: ResultValue<EncodedPacket | null>,
|
||||||
|
packet: EncodedPacket,
|
||||||
|
options: PacketRetrievalOptions,
|
||||||
|
): Promise<Yo> {
|
||||||
|
const regularSampleIndex = packet.sampleIndex;// this.packetToSampleIndex.get(packet);
|
||||||
|
|
||||||
if (regularSampleIndex !== undefined) {
|
if (regularSampleIndex !== undefined) {
|
||||||
// Prefer the non-fragmented packet
|
// 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);
|
const locationInFragment = this.packetToFragmentLocation.get(packet);
|
||||||
if (locationInFragment === undefined) {
|
if (locationInFragment === undefined) {
|
||||||
throw new Error('Packet was not created from this track.');
|
throw new Error('Packet was not created from this track.');
|
||||||
@@ -2483,9 +2513,14 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
Infinity,
|
Infinity,
|
||||||
options,
|
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 timestampInTimescale = this.mapTimestampIntoTimescale(timestamp);
|
||||||
|
|
||||||
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
||||||
@@ -2493,13 +2528,19 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
const keyFrameSampleIndex = sampleIndex === -1
|
const keyFrameSampleIndex = sampleIndex === -1
|
||||||
? -1
|
? -1
|
||||||
: getRelevantKeyframeIndexForSample(sampleTable, sampleIndex);
|
: 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) {
|
if (!sampleTableIsEmpty(sampleTable) || !this.internalTrack.demuxer.isFragmented) {
|
||||||
// Prefer the non-fragmented packet
|
// Prefer the non-fragmented packet
|
||||||
return regularPacket;
|
return res.pass();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error('minusch');
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
return this.performFragmentedLookup(
|
return this.performFragmentedLookup(
|
||||||
null,
|
null,
|
||||||
(fragment) => {
|
(fragment) => {
|
||||||
@@ -2522,15 +2563,20 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
timestampInTimescale,
|
timestampInTimescale,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
|
async getNextKeyPacket(
|
||||||
const regularSampleIndex = this.packetToSampleIndex.get(packet);
|
res: ResultValue<EncodedPacket | null>,
|
||||||
|
packet: EncodedPacket,
|
||||||
|
options: PacketRetrievalOptions,
|
||||||
|
): Promise<Yo> {
|
||||||
|
const regularSampleIndex = packet.sampleIndex;// this.packetToSampleIndex.get(packet);
|
||||||
if (regularSampleIndex !== undefined) {
|
if (regularSampleIndex !== undefined) {
|
||||||
// Prefer the non-fragmented packet
|
// Prefer the non-fragmented packet
|
||||||
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
||||||
const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, regularSampleIndex);
|
const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, regularSampleIndex);
|
||||||
return this.fetchPacketForSampleIndex(nextKeyFrameSampleIndex, options);
|
return this.fetchPacketForSampleIndex(res, nextKeyFrameSampleIndex, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
const locationInFragment = this.packetToFragmentLocation.get(packet);
|
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('Packet was not created from this track.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error('Sie sagen Mel');
|
||||||
|
|
||||||
|
/*
|
||||||
return this.performFragmentedLookup(
|
return this.performFragmentedLookup(
|
||||||
locationInFragment.fragment,
|
locationInFragment.fragment,
|
||||||
(fragment) => {
|
(fragment) => {
|
||||||
@@ -2576,17 +2625,22 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
Infinity,
|
Infinity,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchPacketForSampleIndex(sampleIndex: number, options: PacketRetrievalOptions) {
|
private async fetchPacketForSampleIndex(
|
||||||
|
res: ResultValue<EncodedPacket | null>,
|
||||||
|
sampleIndex: number,
|
||||||
|
options: PacketRetrievalOptions,
|
||||||
|
): Promise<Yo> {
|
||||||
if (sampleIndex === -1) {
|
if (sampleIndex === -1) {
|
||||||
return null;
|
return res.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
||||||
const sampleInfo = getSampleInfo(sampleTable, sampleIndex);
|
const sampleInfo = getSampleInfo(sampleTable, sampleIndex);
|
||||||
if (!sampleInfo) {
|
if (!sampleInfo) {
|
||||||
return null;
|
return res.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: Uint8Array;
|
let data: Uint8Array;
|
||||||
@@ -2615,9 +2669,10 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
|||||||
sampleInfo.sampleSize,
|
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) {
|
private async fetchPacketInFragment(fragment: Fragment, sampleIndex: number, options: PacketRetrievalOptions) {
|
||||||
@@ -2922,8 +2977,6 @@ type SampleInfo = {
|
|||||||
duration: number;
|
duration: number;
|
||||||
sampleOffset: number;
|
sampleOffset: number;
|
||||||
sampleSize: number;
|
sampleSize: number;
|
||||||
chunkOffset: number;
|
|
||||||
chunkSize: number;
|
|
||||||
isKeyFrame: boolean;
|
isKeyFrame: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2958,20 +3011,13 @@ const getSampleInfo = (sampleTable: SampleTable, sampleIndex: number): SampleInf
|
|||||||
|
|
||||||
const startSampleIndexOfChunk = chunkEntry.startSampleIndex
|
const startSampleIndexOfChunk = chunkEntry.startSampleIndex
|
||||||
+ (chunkIndex - chunkEntry.startChunkIndex) * chunkEntry.samplesPerChunk;
|
+ (chunkIndex - chunkEntry.startChunkIndex) * chunkEntry.samplesPerChunk;
|
||||||
let chunkSize = 0;
|
|
||||||
let sampleOffset = chunkOffset;
|
let sampleOffset = chunkOffset;
|
||||||
|
|
||||||
if (sampleTable.sampleSizes.length === 1) {
|
if (sampleTable.sampleSizes.length === 1) {
|
||||||
sampleOffset += sampleSize * (sampleIndex - startSampleIndexOfChunk);
|
sampleOffset += sampleSize * (sampleIndex - startSampleIndexOfChunk);
|
||||||
chunkSize += sampleSize * chunkEntry.samplesPerChunk;
|
|
||||||
} else {
|
} else {
|
||||||
for (let i = startSampleIndexOfChunk; i < startSampleIndexOfChunk + chunkEntry.samplesPerChunk; i++) {
|
for (let i = startSampleIndexOfChunk; i < sampleIndex; i++) {
|
||||||
const sampleSize = sampleTable.sampleSizes[i]!;
|
sampleOffset += sampleTable.sampleSizes[i]!;
|
||||||
|
|
||||||
if (i < sampleIndex) {
|
|
||||||
sampleOffset += sampleSize;
|
|
||||||
}
|
|
||||||
chunkSize += sampleSize;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2994,8 +3040,6 @@ const getSampleInfo = (sampleTable: SampleTable, sampleIndex: number): SampleInf
|
|||||||
duration,
|
duration,
|
||||||
sampleOffset,
|
sampleOffset,
|
||||||
sampleSize,
|
sampleSize,
|
||||||
chunkOffset,
|
|
||||||
chunkSize,
|
|
||||||
isKeyFrame: sampleTable.keySampleIndices
|
isKeyFrame: sampleTable.keySampleIndices
|
||||||
? binarySearchExact(sampleTable.keySampleIndices, sampleIndex, x => x) !== -1
|
? binarySearchExact(sampleTable.keySampleIndices, sampleIndex, x => x) !== -1
|
||||||
: true,
|
: true,
|
||||||
|
|||||||
+19
-4
@@ -32,12 +32,15 @@ import {
|
|||||||
isWebKit,
|
isWebKit,
|
||||||
last,
|
last,
|
||||||
mapAsyncGenerator,
|
mapAsyncGenerator,
|
||||||
|
MaybePromise,
|
||||||
promiseWithResolvers,
|
promiseWithResolvers,
|
||||||
|
ResultValue,
|
||||||
Rotation,
|
Rotation,
|
||||||
toAsyncIterator,
|
toAsyncIterator,
|
||||||
toDataView,
|
toDataView,
|
||||||
toUint8Array,
|
toUint8Array,
|
||||||
validateAnyIterable,
|
validateAnyIterable,
|
||||||
|
Yo,
|
||||||
} from './misc';
|
} from './misc';
|
||||||
import { EncodedPacket } from './packet';
|
import { EncodedPacket } from './packet';
|
||||||
import { fromAlaw, fromUlaw } from './pcm';
|
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
|
* 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.
|
* likely to be a key packet.
|
||||||
*/
|
*/
|
||||||
getFirstPacket(options: PacketRetrievalOptions = {}) {
|
getFirstPacket(options: PacketRetrievalOptions = {}): MaybePromise<EncodedPacket | null> {
|
||||||
validatePacketRetrievalOptions(options);
|
validatePacketRetrievalOptions(options);
|
||||||
|
|
||||||
if (this._track.input._disposed) {
|
if (this._track.input._disposed) {
|
||||||
throw new InputDisposedError();
|
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();
|
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;
|
return decodedSampleQueueSize === 0 ? 40 : 8;
|
||||||
};
|
};
|
||||||
|
|
||||||
class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
|
export class VideoDecoderWrapper extends DecoderWrapper<VideoSample> {
|
||||||
decoder: VideoDecoder | null = null;
|
decoder: VideoDecoder | null = null;
|
||||||
|
|
||||||
customDecoder: CustomVideoDecoder | null = null;
|
customDecoder: CustomVideoDecoder | null = null;
|
||||||
|
|||||||
+43
@@ -796,3 +796,46 @@ export const polyfillSymbolDispose = () => {
|
|||||||
export const isNumber = (x: unknown) => {
|
export const isNumber = (x: unknown) => {
|
||||||
return typeof x === 'number' && !Number.isNaN(x);
|
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
@@ -12,7 +12,7 @@ import { Input } from '../input';
|
|||||||
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
||||||
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
||||||
import { PacketRetrievalOptions } from '../media-sink';
|
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 { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||||
import { FrameHeader, getXingOffset, INFO, XING } from '../../shared/mp3-misc';
|
import { FrameHeader, getXingOffset, INFO, XING } from '../../shared/mp3-misc';
|
||||||
import {
|
import {
|
||||||
@@ -42,7 +42,8 @@ export class Mp3Demuxer extends Demuxer {
|
|||||||
|
|
||||||
tracks: InputAudioTrack[] = [];
|
tracks: InputAudioTrack[] = [];
|
||||||
|
|
||||||
readingMutex = new AsyncMutex();
|
// readingMutex = new AsyncMutex();
|
||||||
|
readingMutex = new AsyncMutex2();
|
||||||
lastSampleLoaded = false;
|
lastSampleLoaded = false;
|
||||||
lastLoadedPos = 0;
|
lastLoadedPos = 0;
|
||||||
nextTimestampInSamples = 0;
|
nextTimestampInSamples = 0;
|
||||||
@@ -57,7 +58,9 @@ export class Mp3Demuxer extends Demuxer {
|
|||||||
return this.metadataPromise ??= (async () => {
|
return this.metadataPromise ??= (async () => {
|
||||||
// Keep loading until we find the first frame header
|
// Keep loading until we find the first frame header
|
||||||
while (!this.firstFrameHeader && !this.lastSampleLoaded) {
|
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) {
|
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) {
|
if (this.lastLoadedPos === 0) {
|
||||||
// Let's skip all ID3v2 tags at the start of the file
|
// Let's skip all ID3v2 tags at the start of the file
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -77,7 +80,7 @@ export class Mp3Demuxer extends Demuxer {
|
|||||||
|
|
||||||
if (!slice) {
|
if (!slice) {
|
||||||
this.lastSampleLoaded = true;
|
this.lastSampleLoaded = true;
|
||||||
return;
|
return res.set();
|
||||||
}
|
}
|
||||||
|
|
||||||
const id3V2Header = readId3V2Header(slice);
|
const id3V2Header = readId3V2Header(slice);
|
||||||
@@ -89,19 +92,25 @@ export class Mp3Demuxer extends Demuxer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await readNextFrameHeader(this.reader, this.lastLoadedPos, this.reader.fileSize);
|
const result = new ResultValue<{
|
||||||
if (!result) {
|
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;
|
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);
|
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 instanceof Promise) slice = await slice;
|
||||||
if (slice) {
|
if (slice) {
|
||||||
const word = readU32Be(slice);
|
const word = readU32Be(slice);
|
||||||
@@ -109,7 +118,7 @@ export class Mp3Demuxer extends Demuxer {
|
|||||||
|
|
||||||
if (isXing) {
|
if (isXing) {
|
||||||
// There's no actual audio data in this frame, so let's skip it
|
// 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 = {
|
const sample: Sample = {
|
||||||
timestamp: this.nextTimestampInSamples / this.firstFrameHeader.sampleRate,
|
timestamp: this.nextTimestampInSamples / this.firstFrameHeader.sampleRate,
|
||||||
duration: sampleDuration,
|
duration: sampleDuration,
|
||||||
dataStart: result.startPos,
|
dataStart: result.value.startPos,
|
||||||
dataSize: header.totalSize,
|
dataSize: header.totalSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.loadedSamples.push(sample);
|
this.loadedSamples.push(sample);
|
||||||
this.nextTimestampInSamples += header.audioSamplesInFrame;
|
this.nextTimestampInSamples += header.audioSamplesInFrame;
|
||||||
|
|
||||||
return;
|
return res.set();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMimeType() {
|
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) {
|
if (sampleIndex === -1) {
|
||||||
return null;
|
return res.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawSample = this.demuxer.loadedSamples[sampleIndex];
|
const rawSample = this.demuxer.loadedSamples[sampleIndex];
|
||||||
if (!rawSample) {
|
if (!rawSample) {
|
||||||
return null;
|
return res.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: Uint8Array;
|
let data: Uint8Array;
|
||||||
@@ -291,28 +300,33 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
|
|||||||
if (slice instanceof Promise) slice = await slice;
|
if (slice instanceof Promise) slice = await slice;
|
||||||
|
|
||||||
if (!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);
|
data = readBytes(slice, rawSample.dataSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new EncodedPacket(
|
return res.set(new EncodedPacket(
|
||||||
data,
|
data,
|
||||||
'key',
|
'key',
|
||||||
rawSample.timestamp,
|
rawSample.timestamp,
|
||||||
rawSample.duration,
|
rawSample.duration,
|
||||||
sampleIndex,
|
sampleIndex,
|
||||||
rawSample.dataSize,
|
rawSample.dataSize,
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
getFirstPacket(options: PacketRetrievalOptions) {
|
getFirstPacket(res: ResultValue<EncodedPacket | null>, options: PacketRetrievalOptions) {
|
||||||
return this.getPacketAtIndex(0, options);
|
return this.getPacketAtIndex(res, 0, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
|
async getNextPacket(res: ResultValue<EncodedPacket | null>, packet: EncodedPacket, options: PacketRetrievalOptions): Promise<Yo> {
|
||||||
const release = await this.demuxer.readingMutex.acquire();
|
// 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 {
|
try {
|
||||||
const sampleIndex = binarySearchExact(
|
const sampleIndex = binarySearchExact(
|
||||||
@@ -330,12 +344,14 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
|
|||||||
nextIndex >= this.demuxer.loadedSamples.length
|
nextIndex >= this.demuxer.loadedSamples.length
|
||||||
&& !this.demuxer.lastSampleLoaded
|
&& !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 {
|
} finally {
|
||||||
release();
|
// release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-6
@@ -7,12 +7,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { FRAME_HEADER_SIZE, FrameHeader, readFrameHeader } from '../../shared/mp3-misc';
|
import { FRAME_HEADER_SIZE, FrameHeader, readFrameHeader } from '../../shared/mp3-misc';
|
||||||
|
import { ResultValue, Yo } from '../misc';
|
||||||
import { Reader, readU32Be } from '../reader';
|
import { Reader, readU32Be } from '../reader';
|
||||||
|
|
||||||
export const readNextFrameHeader = async (reader: Reader, startPos: number, until: number | null): Promise<{
|
export const readNextFrameHeader = async (
|
||||||
header: FrameHeader;
|
res: ResultValue<{
|
||||||
startPos: number;
|
header: FrameHeader;
|
||||||
} | null> => {
|
startPos: number;
|
||||||
|
} | null>,
|
||||||
|
reader: Reader,
|
||||||
|
startPos: number,
|
||||||
|
until: number | null,
|
||||||
|
): Promise<Yo> => {
|
||||||
let currentPos = startPos;
|
let currentPos = startPos;
|
||||||
|
|
||||||
while (until === null || currentPos < until) {
|
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);
|
const result = readFrameHeader(word, reader.fileSize !== null ? reader.fileSize - currentPos : null);
|
||||||
if (result.header) {
|
if (result.header) {
|
||||||
return { header: result.header, startPos: currentPos };
|
return res.set({ header: result.header, startPos: currentPos });
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPos += result.bytesAdvanced;
|
currentPos += result.bytesAdvanced;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return res.set(null);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user