mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 13:53:53 +02:00
Add own encoded sample classes and port the code to them
This commit is contained in:
+60
-5
@@ -1,3 +1,5 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<script src="../dist/metamuxer.js"></script>
|
<script src="../dist/metamuxer.js"></script>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
@@ -30,19 +32,72 @@
|
|||||||
|
|
||||||
fileInput.addEventListener('change', async () => {
|
fileInput.addEventListener('change', async () => {
|
||||||
const file = fileInput.files[0];
|
const file = fileInput.files[0];
|
||||||
const source = new Metamuxer.BlobSource(file);
|
const source = new Metamuxer.BufferSource(await file.arrayBuffer()) ?? new Metamuxer.BlobSource(file);
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
const input = new Metamuxer.Input({
|
const input = new Metamuxer.Input({
|
||||||
formats: Metamuxer.ALL_FORMATS,
|
formats: Metamuxer.ALL_FORMATS,
|
||||||
source
|
source
|
||||||
});
|
});
|
||||||
|
|
||||||
const audioTrack = await input.getPrimaryAudioTrack();
|
const videoTrack = await input.getPrimaryVideoTrack();
|
||||||
const drain = new Metamuxer.EncodedAudioChunkDrain(audioTrack);
|
const drain = new Metamuxer.EncodedVideoSampleDrain(videoTrack);
|
||||||
|
|
||||||
|
for await (const sample of drain.samples()) {
|
||||||
|
//console.log(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.textContent = performance.now() - start;
|
||||||
|
|
||||||
|
/*
|
||||||
|
const videoTrack = await input.getPrimaryVideoTrack();
|
||||||
|
const drain = new Metamuxer.VideoFrameDrain(videoTrack);
|
||||||
|
|
||||||
|
async function* timestamps() {
|
||||||
|
const fromTime = 0;
|
||||||
|
const toTime = await videoTrack.computeDuration();
|
||||||
|
const extractDuration = toTime - fromTime;
|
||||||
|
const drain = new Metamuxer.EncodedVideoSampleDrain(videoTrack);
|
||||||
|
const thumbnailsNeeded = 16;
|
||||||
|
|
||||||
|
for (let i = 0; i < thumbnailsNeeded; i++) {
|
||||||
|
const startTime = (extractDuration * i) / thumbnailsNeeded + fromTime;
|
||||||
|
const endTime = Math.min(
|
||||||
|
(extractDuration * (i + 1)) / thumbnailsNeeded + fromTime,
|
||||||
|
toTime + 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sample = await drain.getSample(startTime, { onlyMetadata: true });
|
||||||
|
|
||||||
|
let bestTimestamp = sample.timestamp;
|
||||||
|
if (sample.type !== 'key') {
|
||||||
|
const nextKeySample = await drain.getNextKeySample(sample, { onlyMetadata: true });
|
||||||
|
if (nextKeySample.timestamp < endTime) {
|
||||||
|
bestTimestamp = nextKeySample.timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield bestTimestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const { frame, timestamp } of drain.framesAtTimestamps(timestamps())) {
|
||||||
|
console.log(frame.timestamp, timestamp)
|
||||||
|
frame.close();
|
||||||
|
}
|
||||||
|
console.log("done")
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
const audioTrack = await input.getPrimaryVideoTrack();
|
||||||
|
const drain = new Metamuxer.EncodedVideoChunkDrain(audioTrack);
|
||||||
|
|
||||||
for await (const chunk of drain.chunks()) {
|
for await (const chunk of drain.chunks()) {
|
||||||
console.log(chunk);
|
//console.log(chunk);
|
||||||
}
|
}
|
||||||
console.log("Done??")
|
|
||||||
|
document.body.textContent = performance.now() - start;
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<script src="../dist/metamuxer.js"></script>
|
<script src="../dist/metamuxer.js"></script>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<input type="file" accept="video/mp4,video/quicktime,audio/x-m4a,video/x-m4v">
|
<input type="file" accept="video/mp4,video/quicktime,audio/x-m4a,video/x-m4v">
|
||||||
|
|
||||||
<div id="player"
|
<div id="player"
|
||||||
|
|||||||
+38
-8
@@ -842,7 +842,7 @@ export const canEncode = (codec: MediaCodec) => {
|
|||||||
return canEncodeSubtitles(codec as SubtitleCodec);
|
return canEncodeSubtitles(codec as SubtitleCodec);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Unhandled codec.');
|
throw new TypeError(`Unknown codec '${codec}'.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
@@ -851,6 +851,19 @@ export const canEncodeVideo = async (codec: VideoCodec, { width = 1280, height =
|
|||||||
height?: number;
|
height?: number;
|
||||||
bitrate?: 1e6;
|
bitrate?: 1e6;
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
|
if (!VIDEO_CODECS.includes(codec)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(width) || width <= 0) {
|
||||||
|
throw new TypeError('width must be a positive integer.');
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(height) || height <= 0) {
|
||||||
|
throw new TypeError('height must be a positive integer.');
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(bitrate) || bitrate <= 0) {
|
||||||
|
throw new TypeError('bitrate must be a positive integer.');
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof VideoEncoder === 'undefined') {
|
if (typeof VideoEncoder === 'undefined') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -872,14 +885,27 @@ export const canEncodeAudio = async (codec: AudioCodec, { numberOfChannels = 2,
|
|||||||
sampleRate?: number;
|
sampleRate?: number;
|
||||||
bitrate?: number;
|
bitrate?: number;
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
if (typeof AudioEncoder === 'undefined') {
|
if (!AUDIO_CODECS.includes(codec)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!Number.isInteger(numberOfChannels) || numberOfChannels <= 0) {
|
||||||
|
throw new TypeError('numberOfChannels must be a positive integer.');
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(sampleRate) || sampleRate <= 0) {
|
||||||
|
throw new TypeError('sampleRate must be a positive integer.');
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(bitrate) || bitrate <= 0) {
|
||||||
|
throw new TypeError('bitrate must be a positive integer.');
|
||||||
|
}
|
||||||
|
|
||||||
if ((PCM_CODECS as readonly string[]).includes(codec)) {
|
if ((PCM_CODECS as readonly string[]).includes(codec)) {
|
||||||
return false; // TODO write encoder
|
return false; // TODO write encoder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof AudioEncoder === 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const support = await AudioEncoder.isConfigSupported({
|
const support = await AudioEncoder.isConfigSupported({
|
||||||
codec: buildAudioCodecString(codec, numberOfChannels, sampleRate),
|
codec: buildAudioCodecString(codec, numberOfChannels, sampleRate),
|
||||||
numberOfChannels,
|
numberOfChannels,
|
||||||
@@ -891,6 +917,16 @@ export const canEncodeAudio = async (codec: AudioCodec, { numberOfChannels = 2,
|
|||||||
return support.supported === true;
|
return support.supported === true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
|
||||||
|
export const canEncodeSubtitles = async (codec: SubtitleCodec) => {
|
||||||
|
if (!SUBTITLE_CODECS.includes(codec)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const getEncodableCodecs = async (): Promise<MediaCodec[]> => {
|
export const getEncodableCodecs = async (): Promise<MediaCodec[]> => {
|
||||||
const [videoCodecs, audioCodecs, subtitleCodecs] = await Promise.all([
|
const [videoCodecs, audioCodecs, subtitleCodecs] = await Promise.all([
|
||||||
@@ -902,12 +938,6 @@ export const getEncodableCodecs = async (): Promise<MediaCodec[]> => {
|
|||||||
return [...videoCodecs, ...audioCodecs, ...subtitleCodecs];
|
return [...videoCodecs, ...audioCodecs, ...subtitleCodecs];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @public */
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
export const canEncodeSubtitles = async (codec: SubtitleCodec) => {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const getEncodableVideoCodecs = async (): Promise<VideoCodec[]> => {
|
export const getEncodableVideoCodecs = async (): Promise<VideoCodec[]> => {
|
||||||
const bools = await Promise.all(VIDEO_CODECS.map(canEncode));
|
const bools = await Promise.all(VIDEO_CODECS.map(canEncode));
|
||||||
|
|||||||
+10
-4
@@ -60,12 +60,18 @@ export {
|
|||||||
export { Input, InputOptions } from './input';
|
export { Input, InputOptions } from './input';
|
||||||
export { InputTrack, InputVideoTrack, InputAudioTrack } from './input-track';
|
export { InputTrack, InputVideoTrack, InputAudioTrack } from './input-track';
|
||||||
export {
|
export {
|
||||||
ChunkRetrievalOptions,
|
EncodedVideoSample,
|
||||||
BaseChunkDrain,
|
EncodedAudioSample,
|
||||||
|
SampleType,
|
||||||
|
} from './sample';
|
||||||
|
export {
|
||||||
|
SampleRetrievalOptions,
|
||||||
|
BaseSampleDrain,
|
||||||
|
WrappedMediaFrame,
|
||||||
BaseMediaFrameDrain,
|
BaseMediaFrameDrain,
|
||||||
EncodedVideoChunkDrain,
|
EncodedVideoSampleDrain,
|
||||||
VideoFrameDrain,
|
VideoFrameDrain,
|
||||||
EncodedAudioChunkDrain,
|
EncodedAudioSampleDrain,
|
||||||
CanvasDrain,
|
CanvasDrain,
|
||||||
WrappedCanvas,
|
WrappedCanvas,
|
||||||
AudioDataDrain,
|
AudioDataDrain,
|
||||||
|
|||||||
+12
-11
@@ -1,6 +1,7 @@
|
|||||||
import { AudioCodec, MediaCodec, VideoCodec } from './codec';
|
import { AudioCodec, MediaCodec, VideoCodec } from './codec';
|
||||||
import { ChunkRetrievalOptions } from './media-drain';
|
import { SampleRetrievalOptions } from './media-drain';
|
||||||
import { Rotation } from './misc';
|
import { Rotation } from './misc';
|
||||||
|
import { EncodedAudioSample, EncodedVideoSample } from './sample';
|
||||||
|
|
||||||
export interface InputTrackBacking {
|
export interface InputTrackBacking {
|
||||||
getCodec(): Promise<MediaCodec | null>;
|
getCodec(): Promise<MediaCodec | null>;
|
||||||
@@ -40,11 +41,11 @@ export interface InputVideoTrackBacking extends InputTrackBacking {
|
|||||||
getCodedHeight(): Promise<number>;
|
getCodedHeight(): Promise<number>;
|
||||||
getRotation(): Promise<Rotation>;
|
getRotation(): Promise<Rotation>;
|
||||||
getDecoderConfig(): Promise<VideoDecoderConfig | null>;
|
getDecoderConfig(): Promise<VideoDecoderConfig | null>;
|
||||||
getFirstChunk(options: ChunkRetrievalOptions): Promise<EncodedVideoChunk | null>;
|
getFirstSample(options: SampleRetrievalOptions): Promise<EncodedVideoSample | null>;
|
||||||
getChunk(timestamp: number, options: ChunkRetrievalOptions): Promise<EncodedVideoChunk | null>;
|
getSample(timestamp: number, options: SampleRetrievalOptions): Promise<EncodedVideoSample | null>;
|
||||||
getNextChunk(chunk: EncodedVideoChunk, options: ChunkRetrievalOptions): Promise<EncodedVideoChunk | null>;
|
getNextSample(sample: EncodedVideoSample, options: SampleRetrievalOptions): Promise<EncodedVideoSample | null>;
|
||||||
getKeyChunk(timestamp: number, options: ChunkRetrievalOptions): Promise<EncodedVideoChunk | null>;
|
getKeySample(timestamp: number, options: SampleRetrievalOptions): Promise<EncodedVideoSample | null>;
|
||||||
getNextKeyChunk(chunk: EncodedVideoChunk, options: ChunkRetrievalOptions): Promise<EncodedVideoChunk | null>;
|
getNextKeySample(sample: EncodedVideoSample, options: SampleRetrievalOptions): Promise<EncodedVideoSample | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
@@ -115,11 +116,11 @@ export interface InputAudioTrackBacking extends InputTrackBacking {
|
|||||||
getNumberOfChannels(): Promise<number>;
|
getNumberOfChannels(): Promise<number>;
|
||||||
getSampleRate(): Promise<number>;
|
getSampleRate(): Promise<number>;
|
||||||
getDecoderConfig(): Promise<AudioDecoderConfig | null>;
|
getDecoderConfig(): Promise<AudioDecoderConfig | null>;
|
||||||
getFirstChunk(options: ChunkRetrievalOptions): Promise<EncodedAudioChunk | null>;
|
getFirstSample(options: SampleRetrievalOptions): Promise<EncodedAudioSample | null>;
|
||||||
getChunk(timestamp: number, options: ChunkRetrievalOptions): Promise<EncodedAudioChunk | null>;
|
getSample(timestamp: number, options: SampleRetrievalOptions): Promise<EncodedAudioSample | null>;
|
||||||
getNextChunk(chunk: EncodedAudioChunk, options: ChunkRetrievalOptions): Promise<EncodedAudioChunk | null>;
|
getNextSample(sample: EncodedAudioSample, options: SampleRetrievalOptions): Promise<EncodedAudioSample | null>;
|
||||||
getKeyChunk(timestamp: number, options: ChunkRetrievalOptions): Promise<EncodedAudioChunk | null>;
|
getKeySample(timestamp: number, options: SampleRetrievalOptions): Promise<EncodedAudioSample | null>;
|
||||||
getNextKeyChunk(chunk: EncodedAudioChunk, options: ChunkRetrievalOptions): Promise<EncodedAudioChunk | null>;
|
getNextKeySample(sample: EncodedAudioSample, options: SampleRetrievalOptions): Promise<EncodedAudioSample | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
|||||||
+100
-83
@@ -19,7 +19,7 @@ import {
|
|||||||
InputVideoTrack,
|
InputVideoTrack,
|
||||||
InputVideoTrackBacking,
|
InputVideoTrackBacking,
|
||||||
} from '../input-track';
|
} from '../input-track';
|
||||||
import { ChunkRetrievalOptions } from '../media-drain';
|
import { SampleRetrievalOptions } from '../media-drain';
|
||||||
import {
|
import {
|
||||||
assert,
|
assert,
|
||||||
COLOR_PRIMARIES_MAP_INVERSE,
|
COLOR_PRIMARIES_MAP_INVERSE,
|
||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
findLastIndex,
|
findLastIndex,
|
||||||
} from '../misc';
|
} from '../misc';
|
||||||
import { Reader } from '../reader';
|
import { Reader } from '../reader';
|
||||||
|
import { EncodedAudioSample, EncodedVideoSample, PLACEHOLDER_DATA, SampleType } from '../sample';
|
||||||
import { IsobmffReader } from './isobmff-reader';
|
import { IsobmffReader } from './isobmff-reader';
|
||||||
|
|
||||||
type InternalTrack = {
|
type InternalTrack = {
|
||||||
@@ -1637,9 +1638,11 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudioChunk> implements InputTrackBacking {
|
abstract class IsobmffTrackBacking<
|
||||||
chunkToSampleIndex = new WeakMap<Chunk, number>();
|
Sample extends EncodedVideoSample | EncodedAudioSample,
|
||||||
chunkToFragmentLocation = new WeakMap<Chunk, {
|
> implements InputTrackBacking {
|
||||||
|
sampleToSampleIndex = new WeakMap<Sample, number>();
|
||||||
|
sampleToFragmentLocation = new WeakMap<Sample, {
|
||||||
fragment: Fragment;
|
fragment: Fragment;
|
||||||
sampleIndex: number;
|
sampleIndex: number;
|
||||||
}>();
|
}>();
|
||||||
@@ -1651,18 +1654,19 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
|
|||||||
}
|
}
|
||||||
|
|
||||||
async computeDuration() {
|
async computeDuration() {
|
||||||
const lastChunk = await this.getChunk(Infinity, { metadataOnly: true });
|
const lastSample = await this.getSample(Infinity, { metadataOnly: true });
|
||||||
return ((lastChunk?.timestamp ?? 0) + (lastChunk?.duration ?? 0)) / 1e6;
|
return (lastSample?.timestamp ?? 0) + (lastSample?.duration ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract createChunk(
|
abstract createSample(
|
||||||
data: Uint8Array,
|
data: Uint8Array,
|
||||||
|
byteLength: number,
|
||||||
|
type: SampleType,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
duration: number,
|
duration: number,
|
||||||
isKeyFrame: boolean,
|
): Sample;
|
||||||
): Chunk;
|
|
||||||
|
|
||||||
async getFirstChunk(options: ChunkRetrievalOptions) {
|
async getFirstSample(options: SampleRetrievalOptions) {
|
||||||
if (this.internalTrack.demuxer.isFragmented) {
|
if (this.internalTrack.demuxer.isFragmented) {
|
||||||
return this.performFragmentedLookup(
|
return this.performFragmentedLookup(
|
||||||
() => {
|
() => {
|
||||||
@@ -1679,24 +1683,24 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.fetchChunkForSampleIndex(0, options);
|
return this.fetchSampleForSampleIndex(0, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private roundToMicrosecond(timestamp: number) {
|
private intoTimescale(timestamp: number) {
|
||||||
// We transform the timestamp so that chunk retrieval behaves expectedly: All chunks returned have a timestamp
|
const result = timestamp * this.internalTrack.timescale;
|
||||||
// that's floored to the microseconds, and that timestamp may be before the actual timestamp. But since the
|
const rounded = Math.round(result);
|
||||||
// actual timestamp is never communicated to the outside, chunk retrieval should work like the timestamp is
|
|
||||||
// exactly equal to its floored version. This means, when we retrieve the chunk for timestamp 0.333333, but the
|
if (Math.abs(1 - (result / rounded)) < 10 * Number.EPSILON) {
|
||||||
// chunk's true, unrounded timestamp is 1/3, then we would not retrieve that chunk, despite the chunk having a
|
// The result is very close to an integer, meaning the number likely originated by an integer being divided
|
||||||
// floored timestamp of 0.333333. That's why we transform the search timestamp by first flooring it to the
|
// by the timescale. For stability, it's best to return the integer in this case.
|
||||||
// microsecond, and then adding "1-eps" to it to make sure get all chunks whose timestamps will round down to
|
return rounded;
|
||||||
// a value included by the search timestamp.
|
|
||||||
return (Math.floor(timestamp * 1e6) + 0.99999999) / 1e6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChunk(timestamp: number, options: ChunkRetrievalOptions) {
|
return result;
|
||||||
timestamp = this.roundToMicrosecond(timestamp);
|
}
|
||||||
const timestampInTimescale = timestamp * this.internalTrack.timescale;
|
|
||||||
|
async getSample(timestamp: number, options: SampleRetrievalOptions) {
|
||||||
|
const timestampInTimescale = this.intoTimescale(timestamp);
|
||||||
|
|
||||||
if (this.internalTrack.demuxer.isFragmented) {
|
if (this.internalTrack.demuxer.isFragmented) {
|
||||||
return this.performFragmentedLookup(
|
return this.performFragmentedLookup(
|
||||||
@@ -1708,19 +1712,19 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
|
|||||||
} else {
|
} else {
|
||||||
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);
|
||||||
return this.fetchChunkForSampleIndex(sampleIndex, options);
|
return this.fetchSampleForSampleIndex(sampleIndex, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextChunk(chunk: Chunk, options: ChunkRetrievalOptions) {
|
async getNextSample(sample: Sample, options: SampleRetrievalOptions) {
|
||||||
if (this.internalTrack.demuxer.isFragmented) {
|
if (this.internalTrack.demuxer.isFragmented) {
|
||||||
const locationInFragment = this.chunkToFragmentLocation.get(chunk);
|
const locationInFragment = this.sampleToFragmentLocation.get(sample);
|
||||||
if (locationInFragment === undefined) {
|
if (locationInFragment === undefined) {
|
||||||
throw new Error('Chunk was not created from this track.');
|
throw new Error('Sample was not created from this track.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id)!;
|
const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id)!;
|
||||||
const sample = trackData.samples[locationInFragment.sampleIndex]!;
|
const fragmentSample = trackData.samples[locationInFragment.sampleIndex]!;
|
||||||
|
|
||||||
const fragmentIndex = binarySearchExact(
|
const fragmentIndex = binarySearchExact(
|
||||||
this.internalTrack.fragments,
|
this.internalTrack.fragments,
|
||||||
@@ -1767,22 +1771,21 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sample.presentationTimestamp,
|
fragmentSample.presentationTimestamp,
|
||||||
Infinity,
|
Infinity,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sampleIndex = this.chunkToSampleIndex.get(chunk);
|
const sampleIndex = this.sampleToSampleIndex.get(sample);
|
||||||
if (sampleIndex === undefined) {
|
if (sampleIndex === undefined) {
|
||||||
throw new Error('Chunk was not created from this track.');
|
throw new Error('Sample was not created from this track.');
|
||||||
}
|
}
|
||||||
return this.fetchChunkForSampleIndex(sampleIndex + 1, options);
|
return this.fetchSampleForSampleIndex(sampleIndex + 1, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getKeyChunk(timestamp: number, options: ChunkRetrievalOptions) {
|
async getKeySample(timestamp: number, options: SampleRetrievalOptions) {
|
||||||
timestamp = this.roundToMicrosecond(timestamp);
|
const timestampInTimescale = this.intoTimescale(timestamp);
|
||||||
const timestampInTimescale = timestamp * this.internalTrack.timescale;
|
|
||||||
|
|
||||||
if (this.internalTrack.demuxer.isFragmented) {
|
if (this.internalTrack.demuxer.isFragmented) {
|
||||||
return this.performFragmentedLookup(
|
return this.performFragmentedLookup(
|
||||||
@@ -1798,18 +1801,18 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
|
|||||||
const keyFrameSampleIndex = sampleIndex === -1
|
const keyFrameSampleIndex = sampleIndex === -1
|
||||||
? -1
|
? -1
|
||||||
: getRelevantKeyframeIndexForSample(sampleTable, sampleIndex);
|
: getRelevantKeyframeIndexForSample(sampleTable, sampleIndex);
|
||||||
return this.fetchChunkForSampleIndex(keyFrameSampleIndex, options);
|
return this.fetchSampleForSampleIndex(keyFrameSampleIndex, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextKeyChunk(chunk: Chunk, options: ChunkRetrievalOptions) {
|
async getNextKeySample(sample: Sample, options: SampleRetrievalOptions) {
|
||||||
if (this.internalTrack.demuxer.isFragmented) {
|
if (this.internalTrack.demuxer.isFragmented) {
|
||||||
const locationInFragment = this.chunkToFragmentLocation.get(chunk);
|
const locationInFragment = this.sampleToFragmentLocation.get(sample);
|
||||||
if (locationInFragment === undefined) {
|
if (locationInFragment === undefined) {
|
||||||
throw new Error('Chunk was not created from this track.');
|
throw new Error('Sample was not created from this track.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id)!;
|
const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id)!;
|
||||||
const sample = trackData.samples[locationInFragment.sampleIndex]!;
|
const fragmentSample = trackData.samples[locationInFragment.sampleIndex]!;
|
||||||
|
|
||||||
const fragmentIndex = binarySearchExact(
|
const fragmentIndex = binarySearchExact(
|
||||||
this.internalTrack.fragments,
|
this.internalTrack.fragments,
|
||||||
@@ -1866,22 +1869,22 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sample.presentationTimestamp,
|
fragmentSample.presentationTimestamp,
|
||||||
Infinity,
|
Infinity,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sampleIndex = this.chunkToSampleIndex.get(chunk);
|
const sampleIndex = this.sampleToSampleIndex.get(sample);
|
||||||
if (sampleIndex === undefined) {
|
if (sampleIndex === undefined) {
|
||||||
throw new Error('Chunk was not created from this track.');
|
throw new Error('Sample was not created from this track.');
|
||||||
}
|
}
|
||||||
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
|
||||||
const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, sampleIndex);
|
const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, sampleIndex);
|
||||||
return this.fetchChunkForSampleIndex(nextKeyFrameSampleIndex, options);
|
return this.fetchSampleForSampleIndex(nextKeyFrameSampleIndex, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchChunkForSampleIndex(sampleIndex: number, options: ChunkRetrievalOptions) {
|
private async fetchSampleForSampleIndex(sampleIndex: number, options: SampleRetrievalOptions) {
|
||||||
if (sampleIndex === -1) {
|
if (sampleIndex === -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1894,7 +1897,7 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
|
|||||||
|
|
||||||
let data: Uint8Array;
|
let data: Uint8Array;
|
||||||
if (options.metadataOnly) {
|
if (options.metadataOnly) {
|
||||||
data = new Uint8Array(0); // Placeholder buffer
|
data = PLACEHOLDER_DATA;
|
||||||
} else {
|
} else {
|
||||||
// Load the entire chunk
|
// Load the entire chunk
|
||||||
await this.internalTrack.demuxer.chunkReader.reader.loadRange(
|
await this.internalTrack.demuxer.chunkReader.reader.loadRange(
|
||||||
@@ -1906,42 +1909,54 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
|
|||||||
data = this.internalTrack.demuxer.chunkReader.readBytes(sampleInfo.sampleSize);
|
data = this.internalTrack.demuxer.chunkReader.readBytes(sampleInfo.sampleSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = 1e6 * sampleInfo.presentationTimestamp / this.internalTrack.timescale;
|
const timestamp = sampleInfo.presentationTimestamp / this.internalTrack.timescale;
|
||||||
const duration = 1e6 * sampleInfo.duration / this.internalTrack.timescale;
|
const duration = sampleInfo.duration / this.internalTrack.timescale;
|
||||||
const chunk = this.createChunk(data, timestamp, duration, sampleInfo.isKeyFrame);
|
const sample = this.createSample(
|
||||||
|
data,
|
||||||
|
sampleInfo.sampleSize,
|
||||||
|
sampleInfo.isKeyFrame ? 'key' : 'delta',
|
||||||
|
timestamp,
|
||||||
|
duration,
|
||||||
|
);
|
||||||
|
|
||||||
this.chunkToSampleIndex.set(chunk, sampleIndex);
|
this.sampleToSampleIndex.set(sample, sampleIndex);
|
||||||
|
|
||||||
return chunk;
|
return sample;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchChunkInFragment(fragment: Fragment, sampleIndex: number, options: ChunkRetrievalOptions) {
|
private async fetchSampleInFragment(fragment: Fragment, sampleIndex: number, options: SampleRetrievalOptions) {
|
||||||
if (sampleIndex === -1) {
|
if (sampleIndex === -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackData = fragment.trackData.get(this.internalTrack.id)!;
|
const trackData = fragment.trackData.get(this.internalTrack.id)!;
|
||||||
const sample = trackData.samples[sampleIndex];
|
const fragmentSample = trackData.samples[sampleIndex];
|
||||||
assert(sample);
|
assert(fragmentSample);
|
||||||
|
|
||||||
let data: Uint8Array;
|
let data: Uint8Array;
|
||||||
if (options.metadataOnly) {
|
if (options.metadataOnly) {
|
||||||
data = new Uint8Array(0); // Placeholder buffer
|
data = PLACEHOLDER_DATA;
|
||||||
} else {
|
} else {
|
||||||
// Load the entire fragment
|
// Load the entire fragment
|
||||||
await this.internalTrack.demuxer.chunkReader.reader.loadRange(fragment.dataStart, fragment.dataEnd);
|
await this.internalTrack.demuxer.chunkReader.reader.loadRange(fragment.dataStart, fragment.dataEnd);
|
||||||
|
|
||||||
this.internalTrack.demuxer.chunkReader.pos = sample.byteOffset;
|
this.internalTrack.demuxer.chunkReader.pos = fragmentSample.byteOffset;
|
||||||
data = this.internalTrack.demuxer.chunkReader.readBytes(sample.byteSize);
|
data = this.internalTrack.demuxer.chunkReader.readBytes(fragmentSample.byteSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamp = 1e6 * sample.presentationTimestamp / this.internalTrack.timescale;
|
const timestamp = fragmentSample.presentationTimestamp / this.internalTrack.timescale;
|
||||||
const duration = 1e6 * sample.duration / this.internalTrack.timescale;
|
const duration = fragmentSample.duration / this.internalTrack.timescale;
|
||||||
const chunk = this.createChunk(data, timestamp, duration, sample.isKeyFrame);
|
const sample = this.createSample(
|
||||||
|
data,
|
||||||
|
fragmentSample.byteSize,
|
||||||
|
fragmentSample.isKeyFrame ? 'key' : 'delta',
|
||||||
|
timestamp,
|
||||||
|
duration,
|
||||||
|
);
|
||||||
|
|
||||||
this.chunkToFragmentLocation.set(chunk, { fragment, sampleIndex });
|
this.sampleToFragmentLocation.set(sample, { fragment, sampleIndex });
|
||||||
|
|
||||||
return chunk;
|
return sample;
|
||||||
}
|
}
|
||||||
|
|
||||||
private findSampleInFragmentsForTimestamp(timestampInTimescale: number) {
|
private findSampleInFragmentsForTimestamp(timestampInTimescale: number) {
|
||||||
@@ -2009,7 +2024,7 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
|
|||||||
searchTimestamp: number,
|
searchTimestamp: number,
|
||||||
// The timestamp for which we know the correct sample will not come after it
|
// The timestamp for which we know the correct sample will not come after it
|
||||||
latestTimestamp: number,
|
latestTimestamp: number,
|
||||||
options: ChunkRetrievalOptions,
|
options: SampleRetrievalOptions,
|
||||||
) {
|
) {
|
||||||
const demuxer = this.internalTrack.demuxer;
|
const demuxer = this.internalTrack.demuxer;
|
||||||
const release = await demuxer.fragmentLookupMutex.acquire(); // The algorithm requires exclusivity
|
const release = await demuxer.fragmentLookupMutex.acquire(); // The algorithm requires exclusivity
|
||||||
@@ -2019,7 +2034,7 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
|
|||||||
if (correctSampleFound) {
|
if (correctSampleFound) {
|
||||||
// The correct sample already exists, easy path.
|
// The correct sample already exists, easy path.
|
||||||
const fragment = this.internalTrack.fragments[fragmentIndex]!;
|
const fragment = this.internalTrack.fragments[fragmentIndex]!;
|
||||||
return this.fetchChunkInFragment(fragment, sampleIndex, options);
|
return this.fetchSampleInFragment(fragment, sampleIndex, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isobmffReader = demuxer.isobmffReader;
|
const isobmffReader = demuxer.isobmffReader;
|
||||||
@@ -2101,7 +2116,7 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
|
|||||||
const { fragmentIndex, sampleIndex, correctSampleFound } = getBestMatch();
|
const { fragmentIndex, sampleIndex, correctSampleFound } = getBestMatch();
|
||||||
if (correctSampleFound) {
|
if (correctSampleFound) {
|
||||||
const fragment = this.internalTrack.fragments[fragmentIndex]!;
|
const fragment = this.internalTrack.fragments[fragmentIndex]!;
|
||||||
return this.fetchChunkInFragment(fragment, sampleIndex, options);
|
return this.fetchSampleInFragment(fragment, sampleIndex, options);
|
||||||
}
|
}
|
||||||
if (fragmentIndex !== -1) {
|
if (fragmentIndex !== -1) {
|
||||||
bestFragmentIndex = fragmentIndex;
|
bestFragmentIndex = fragmentIndex;
|
||||||
@@ -2115,7 +2130,7 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
|
|||||||
if (bestFragmentIndex !== -1) {
|
if (bestFragmentIndex !== -1) {
|
||||||
// If we finished looping but didn't find a perfect match, still return the best match we found
|
// If we finished looping but didn't find a perfect match, still return the best match we found
|
||||||
const fragment = this.internalTrack.fragments[bestFragmentIndex]!;
|
const fragment = this.internalTrack.fragments[bestFragmentIndex]!;
|
||||||
return this.fetchChunkInFragment(fragment, bestSampleIndex, options);
|
return this.fetchSampleInFragment(fragment, bestSampleIndex, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -2125,7 +2140,7 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IsobmffVideoTrackBacking extends IsobmffTrackBacking<EncodedVideoChunk> implements InputVideoTrackBacking {
|
class IsobmffVideoTrackBacking extends IsobmffTrackBacking<EncodedVideoSample> implements InputVideoTrackBacking {
|
||||||
override internalTrack: InternalVideoTrack;
|
override internalTrack: InternalVideoTrack;
|
||||||
|
|
||||||
constructor(internalTrack: InternalVideoTrack) {
|
constructor(internalTrack: InternalVideoTrack) {
|
||||||
@@ -2163,17 +2178,18 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking<EncodedVideoChunk> im
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
createChunk(data: Uint8Array, timestamp: number, duration: number, isKeyFrame: boolean) {
|
createSample(
|
||||||
return new EncodedVideoChunk({
|
data: Uint8Array,
|
||||||
data,
|
byteLength: number,
|
||||||
timestamp,
|
type: SampleType,
|
||||||
duration,
|
timestamp: number,
|
||||||
type: isKeyFrame ? 'key' : 'delta',
|
duration: number,
|
||||||
});
|
) {
|
||||||
|
return new EncodedVideoSample(data, type, timestamp, duration, byteLength);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IsobmffAudioTrackBacking extends IsobmffTrackBacking<EncodedAudioChunk> implements InputAudioTrackBacking {
|
class IsobmffAudioTrackBacking extends IsobmffTrackBacking<EncodedAudioSample> implements InputAudioTrackBacking {
|
||||||
override internalTrack: InternalAudioTrack;
|
override internalTrack: InternalAudioTrack;
|
||||||
|
|
||||||
constructor(internalTrack: InternalAudioTrack) {
|
constructor(internalTrack: InternalAudioTrack) {
|
||||||
@@ -2206,13 +2222,14 @@ class IsobmffAudioTrackBacking extends IsobmffTrackBacking<EncodedAudioChunk> im
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
createChunk(data: Uint8Array, timestamp: number, duration: number, isKeyFrame: boolean) {
|
createSample(
|
||||||
return new EncodedAudioChunk({
|
data: Uint8Array,
|
||||||
data,
|
byteLength: number,
|
||||||
timestamp,
|
type: SampleType,
|
||||||
duration,
|
timestamp: number,
|
||||||
type: isKeyFrame ? 'key' : 'delta',
|
duration: number,
|
||||||
});
|
) {
|
||||||
|
return new EncodedAudioSample(data, type, timestamp, duration, byteLength);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+368
-319
File diff suppressed because it is too large
Load Diff
+16
-2
@@ -34,11 +34,25 @@ export class Reader {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const encasingSegmentExists = this.loadedSegments.some(x => x.start <= start && x.end >= end);
|
const index = binarySearchLessOrEqual(
|
||||||
if (encasingSegmentExists) {
|
this.loadedSegments,
|
||||||
|
start,
|
||||||
|
x => x.start,
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
for (let i = index; i < this.loadedSegments.length; i++) {
|
||||||
|
const segment = this.loadedSegments[i]!;
|
||||||
|
if (segment.start > start) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentEncasesRequestedRange = segment.end >= end;
|
||||||
|
if (segmentEncasesRequestedRange) {
|
||||||
// Nothing to load
|
// Nothing to load
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const bytesPromise = this.source._read(start, end);
|
const bytesPromise = this.source._read(start, end);
|
||||||
const loadingSegment: LoadingSegment = { start, end, promise: bytesPromise };
|
const loadingSegment: LoadingSegment = { start, end, promise: bytesPromise };
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
export const PLACEHOLDER_DATA = new Uint8Array(0);
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type SampleType = 'key' | 'delta';
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export class EncodedVideoSample {
|
||||||
|
constructor(
|
||||||
|
public readonly data: Uint8Array,
|
||||||
|
public readonly type: SampleType,
|
||||||
|
public readonly timestamp: number,
|
||||||
|
public readonly duration: number,
|
||||||
|
public readonly byteLength = data.byteLength,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get isMetadataOnly() {
|
||||||
|
return this.data === PLACEHOLDER_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
get microsecondTimestamp() {
|
||||||
|
return Math.floor(1e6 * this.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
get microsecondDuration() {
|
||||||
|
return Math.floor(1e6 * this.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
toEncodedVideoChunk() {
|
||||||
|
if (this.isMetadataOnly) {
|
||||||
|
throw new TypeError('Metadata-only samples cannot be converted to a chunk.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof EncodedVideoChunk === 'undefined') {
|
||||||
|
throw new Error('Your browser does not support EncodedVideoChunk.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EncodedVideoChunk({
|
||||||
|
data: this.data,
|
||||||
|
type: this.type,
|
||||||
|
timestamp: this.microsecondTimestamp,
|
||||||
|
duration: this.microsecondDuration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export class EncodedAudioSample {
|
||||||
|
constructor(
|
||||||
|
public readonly data: Uint8Array,
|
||||||
|
public readonly type: SampleType,
|
||||||
|
public readonly timestamp: number,
|
||||||
|
public readonly duration: number,
|
||||||
|
public readonly byteLength = data.byteLength,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get isMetadataOnly() {
|
||||||
|
return this.data === PLACEHOLDER_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
get microsecondTimestamp() {
|
||||||
|
return Math.floor(1e6 * this.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
get microsecondDuration() {
|
||||||
|
return Math.floor(1e6 * this.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
toEncodedAudioChunk() {
|
||||||
|
if (this.isMetadataOnly) {
|
||||||
|
throw new TypeError('Metadata-only samples cannot be converted to a chunk.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof EncodedAudioChunk === 'undefined') {
|
||||||
|
throw new Error('Your browser does not support EncodedAudioChunk.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EncodedAudioChunk({
|
||||||
|
data: this.data,
|
||||||
|
type: this.type,
|
||||||
|
timestamp: this.microsecondTimestamp,
|
||||||
|
duration: this.microsecondDuration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user