Add own encoded sample classes and port the code to them

This commit is contained in:
Vanilagy
2025-01-04 00:14:34 +01:00
parent f6d12390cd
commit e83356662e
10 changed files with 694 additions and 434 deletions
+38 -8
View File
@@ -842,7 +842,7 @@ export const canEncode = (codec: MediaCodec) => {
return canEncodeSubtitles(codec as SubtitleCodec);
}
throw new Error('Unhandled codec.');
throw new TypeError(`Unknown codec '${codec}'.`);
};
/** @public */
@@ -851,6 +851,19 @@ export const canEncodeVideo = async (codec: VideoCodec, { width = 1280, height =
height?: number;
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') {
return false;
}
@@ -872,14 +885,27 @@ export const canEncodeAudio = async (codec: AudioCodec, { numberOfChannels = 2,
sampleRate?: number;
bitrate?: number;
} = {}) => {
if (typeof AudioEncoder === 'undefined') {
if (!AUDIO_CODECS.includes(codec)) {
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)) {
return false; // TODO write encoder
}
if (typeof AudioEncoder === 'undefined') {
return false;
}
const support = await AudioEncoder.isConfigSupported({
codec: buildAudioCodecString(codec, numberOfChannels, sampleRate),
numberOfChannels,
@@ -891,6 +917,16 @@ export const canEncodeAudio = async (codec: AudioCodec, { numberOfChannels = 2,
return support.supported === true;
};
/** @public */
export const canEncodeSubtitles = async (codec: SubtitleCodec) => {
if (!SUBTITLE_CODECS.includes(codec)) {
return false;
}
return true;
};
/** @public */
export const getEncodableCodecs = async (): Promise<MediaCodec[]> => {
const [videoCodecs, audioCodecs, subtitleCodecs] = await Promise.all([
@@ -902,12 +938,6 @@ export const getEncodableCodecs = async (): Promise<MediaCodec[]> => {
return [...videoCodecs, ...audioCodecs, ...subtitleCodecs];
};
/** @public */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const canEncodeSubtitles = async (codec: SubtitleCodec) => {
return true;
};
/** @public */
export const getEncodableVideoCodecs = async (): Promise<VideoCodec[]> => {
const bools = await Promise.all(VIDEO_CODECS.map(canEncode));
+10 -4
View File
@@ -60,12 +60,18 @@ export {
export { Input, InputOptions } from './input';
export { InputTrack, InputVideoTrack, InputAudioTrack } from './input-track';
export {
ChunkRetrievalOptions,
BaseChunkDrain,
EncodedVideoSample,
EncodedAudioSample,
SampleType,
} from './sample';
export {
SampleRetrievalOptions,
BaseSampleDrain,
WrappedMediaFrame,
BaseMediaFrameDrain,
EncodedVideoChunkDrain,
EncodedVideoSampleDrain,
VideoFrameDrain,
EncodedAudioChunkDrain,
EncodedAudioSampleDrain,
CanvasDrain,
WrappedCanvas,
AudioDataDrain,
+12 -11
View File
@@ -1,6 +1,7 @@
import { AudioCodec, MediaCodec, VideoCodec } from './codec';
import { ChunkRetrievalOptions } from './media-drain';
import { SampleRetrievalOptions } from './media-drain';
import { Rotation } from './misc';
import { EncodedAudioSample, EncodedVideoSample } from './sample';
export interface InputTrackBacking {
getCodec(): Promise<MediaCodec | null>;
@@ -40,11 +41,11 @@ export interface InputVideoTrackBacking extends InputTrackBacking {
getCodedHeight(): Promise<number>;
getRotation(): Promise<Rotation>;
getDecoderConfig(): Promise<VideoDecoderConfig | null>;
getFirstChunk(options: ChunkRetrievalOptions): Promise<EncodedVideoChunk | null>;
getChunk(timestamp: number, options: ChunkRetrievalOptions): Promise<EncodedVideoChunk | null>;
getNextChunk(chunk: EncodedVideoChunk, options: ChunkRetrievalOptions): Promise<EncodedVideoChunk | null>;
getKeyChunk(timestamp: number, options: ChunkRetrievalOptions): Promise<EncodedVideoChunk | null>;
getNextKeyChunk(chunk: EncodedVideoChunk, options: ChunkRetrievalOptions): Promise<EncodedVideoChunk | null>;
getFirstSample(options: SampleRetrievalOptions): Promise<EncodedVideoSample | null>;
getSample(timestamp: number, options: SampleRetrievalOptions): Promise<EncodedVideoSample | null>;
getNextSample(sample: EncodedVideoSample, options: SampleRetrievalOptions): Promise<EncodedVideoSample | null>;
getKeySample(timestamp: number, options: SampleRetrievalOptions): Promise<EncodedVideoSample | null>;
getNextKeySample(sample: EncodedVideoSample, options: SampleRetrievalOptions): Promise<EncodedVideoSample | null>;
}
/** @public */
@@ -115,11 +116,11 @@ export interface InputAudioTrackBacking extends InputTrackBacking {
getNumberOfChannels(): Promise<number>;
getSampleRate(): Promise<number>;
getDecoderConfig(): Promise<AudioDecoderConfig | null>;
getFirstChunk(options: ChunkRetrievalOptions): Promise<EncodedAudioChunk | null>;
getChunk(timestamp: number, options: ChunkRetrievalOptions): Promise<EncodedAudioChunk | null>;
getNextChunk(chunk: EncodedAudioChunk, options: ChunkRetrievalOptions): Promise<EncodedAudioChunk | null>;
getKeyChunk(timestamp: number, options: ChunkRetrievalOptions): Promise<EncodedAudioChunk | null>;
getNextKeyChunk(chunk: EncodedAudioChunk, options: ChunkRetrievalOptions): Promise<EncodedAudioChunk | null>;
getFirstSample(options: SampleRetrievalOptions): Promise<EncodedAudioSample | null>;
getSample(timestamp: number, options: SampleRetrievalOptions): Promise<EncodedAudioSample | null>;
getNextSample(sample: EncodedAudioSample, options: SampleRetrievalOptions): Promise<EncodedAudioSample | null>;
getKeySample(timestamp: number, options: SampleRetrievalOptions): Promise<EncodedAudioSample | null>;
getNextKeySample(sample: EncodedAudioSample, options: SampleRetrievalOptions): Promise<EncodedAudioSample | null>;
}
/** @public */
+100 -83
View File
@@ -19,7 +19,7 @@ import {
InputVideoTrack,
InputVideoTrackBacking,
} from '../input-track';
import { ChunkRetrievalOptions } from '../media-drain';
import { SampleRetrievalOptions } from '../media-drain';
import {
assert,
COLOR_PRIMARIES_MAP_INVERSE,
@@ -34,6 +34,7 @@ import {
findLastIndex,
} from '../misc';
import { Reader } from '../reader';
import { EncodedAudioSample, EncodedVideoSample, PLACEHOLDER_DATA, SampleType } from '../sample';
import { IsobmffReader } from './isobmff-reader';
type InternalTrack = {
@@ -1637,9 +1638,11 @@ export class IsobmffDemuxer extends Demuxer {
}
}
abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudioChunk> implements InputTrackBacking {
chunkToSampleIndex = new WeakMap<Chunk, number>();
chunkToFragmentLocation = new WeakMap<Chunk, {
abstract class IsobmffTrackBacking<
Sample extends EncodedVideoSample | EncodedAudioSample,
> implements InputTrackBacking {
sampleToSampleIndex = new WeakMap<Sample, number>();
sampleToFragmentLocation = new WeakMap<Sample, {
fragment: Fragment;
sampleIndex: number;
}>();
@@ -1651,18 +1654,19 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
}
async computeDuration() {
const lastChunk = await this.getChunk(Infinity, { metadataOnly: true });
return ((lastChunk?.timestamp ?? 0) + (lastChunk?.duration ?? 0)) / 1e6;
const lastSample = await this.getSample(Infinity, { metadataOnly: true });
return (lastSample?.timestamp ?? 0) + (lastSample?.duration ?? 0);
}
abstract createChunk(
abstract createSample(
data: Uint8Array,
byteLength: number,
type: SampleType,
timestamp: number,
duration: number,
isKeyFrame: boolean,
): Chunk;
): Sample;
async getFirstChunk(options: ChunkRetrievalOptions) {
async getFirstSample(options: SampleRetrievalOptions) {
if (this.internalTrack.demuxer.isFragmented) {
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) {
// We transform the timestamp so that chunk retrieval behaves expectedly: All chunks returned have a timestamp
// that's floored to the microseconds, and that timestamp may be before the actual timestamp. But since the
// 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
// chunk's true, unrounded timestamp is 1/3, then we would not retrieve that chunk, despite the chunk having a
// floored timestamp of 0.333333. That's why we transform the search timestamp by first flooring it to the
// microsecond, and then adding "1-eps" to it to make sure get all chunks whose timestamps will round down to
// a value included by the search timestamp.
return (Math.floor(timestamp * 1e6) + 0.99999999) / 1e6;
private intoTimescale(timestamp: number) {
const result = timestamp * this.internalTrack.timescale;
const rounded = Math.round(result);
if (Math.abs(1 - (result / rounded)) < 10 * Number.EPSILON) {
// The result is very close to an integer, meaning the number likely originated by an integer being divided
// by the timescale. For stability, it's best to return the integer in this case.
return rounded;
}
return result;
}
async getChunk(timestamp: number, options: ChunkRetrievalOptions) {
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) {
return this.performFragmentedLookup(
@@ -1708,19 +1712,19 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
} else {
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
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) {
const locationInFragment = this.chunkToFragmentLocation.get(chunk);
const locationInFragment = this.sampleToFragmentLocation.get(sample);
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 sample = trackData.samples[locationInFragment.sampleIndex]!;
const fragmentSample = trackData.samples[locationInFragment.sampleIndex]!;
const fragmentIndex = binarySearchExact(
this.internalTrack.fragments,
@@ -1767,22 +1771,21 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
};
}
},
sample.presentationTimestamp,
fragmentSample.presentationTimestamp,
Infinity,
options,
);
}
const sampleIndex = this.chunkToSampleIndex.get(chunk);
const sampleIndex = this.sampleToSampleIndex.get(sample);
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) {
timestamp = this.roundToMicrosecond(timestamp);
const timestampInTimescale = timestamp * this.internalTrack.timescale;
async getKeySample(timestamp: number, options: SampleRetrievalOptions) {
const timestampInTimescale = this.intoTimescale(timestamp);
if (this.internalTrack.demuxer.isFragmented) {
return this.performFragmentedLookup(
@@ -1798,18 +1801,18 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
const keyFrameSampleIndex = sampleIndex === -1
? -1
: 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) {
const locationInFragment = this.chunkToFragmentLocation.get(chunk);
const locationInFragment = this.sampleToFragmentLocation.get(sample);
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 sample = trackData.samples[locationInFragment.sampleIndex]!;
const fragmentSample = trackData.samples[locationInFragment.sampleIndex]!;
const fragmentIndex = binarySearchExact(
this.internalTrack.fragments,
@@ -1866,22 +1869,22 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
};
}
},
sample.presentationTimestamp,
fragmentSample.presentationTimestamp,
Infinity,
options,
);
}
const sampleIndex = this.chunkToSampleIndex.get(chunk);
const sampleIndex = this.sampleToSampleIndex.get(sample);
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 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) {
return null;
}
@@ -1894,7 +1897,7 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
let data: Uint8Array;
if (options.metadataOnly) {
data = new Uint8Array(0); // Placeholder buffer
data = PLACEHOLDER_DATA;
} else {
// Load the entire chunk
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);
}
const timestamp = 1e6 * sampleInfo.presentationTimestamp / this.internalTrack.timescale;
const duration = 1e6 * sampleInfo.duration / this.internalTrack.timescale;
const chunk = this.createChunk(data, timestamp, duration, sampleInfo.isKeyFrame);
const timestamp = sampleInfo.presentationTimestamp / this.internalTrack.timescale;
const duration = sampleInfo.duration / this.internalTrack.timescale;
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) {
return null;
}
const trackData = fragment.trackData.get(this.internalTrack.id)!;
const sample = trackData.samples[sampleIndex];
assert(sample);
const fragmentSample = trackData.samples[sampleIndex];
assert(fragmentSample);
let data: Uint8Array;
if (options.metadataOnly) {
data = new Uint8Array(0); // Placeholder buffer
data = PLACEHOLDER_DATA;
} else {
// Load the entire fragment
await this.internalTrack.demuxer.chunkReader.reader.loadRange(fragment.dataStart, fragment.dataEnd);
this.internalTrack.demuxer.chunkReader.pos = sample.byteOffset;
data = this.internalTrack.demuxer.chunkReader.readBytes(sample.byteSize);
this.internalTrack.demuxer.chunkReader.pos = fragmentSample.byteOffset;
data = this.internalTrack.demuxer.chunkReader.readBytes(fragmentSample.byteSize);
}
const timestamp = 1e6 * sample.presentationTimestamp / this.internalTrack.timescale;
const duration = 1e6 * sample.duration / this.internalTrack.timescale;
const chunk = this.createChunk(data, timestamp, duration, sample.isKeyFrame);
const timestamp = fragmentSample.presentationTimestamp / this.internalTrack.timescale;
const duration = fragmentSample.duration / this.internalTrack.timescale;
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) {
@@ -2009,7 +2024,7 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
searchTimestamp: number,
// The timestamp for which we know the correct sample will not come after it
latestTimestamp: number,
options: ChunkRetrievalOptions,
options: SampleRetrievalOptions,
) {
const demuxer = this.internalTrack.demuxer;
const release = await demuxer.fragmentLookupMutex.acquire(); // The algorithm requires exclusivity
@@ -2019,7 +2034,7 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
if (correctSampleFound) {
// The correct sample already exists, easy path.
const fragment = this.internalTrack.fragments[fragmentIndex]!;
return this.fetchChunkInFragment(fragment, sampleIndex, options);
return this.fetchSampleInFragment(fragment, sampleIndex, options);
}
const isobmffReader = demuxer.isobmffReader;
@@ -2101,7 +2116,7 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
const { fragmentIndex, sampleIndex, correctSampleFound } = getBestMatch();
if (correctSampleFound) {
const fragment = this.internalTrack.fragments[fragmentIndex]!;
return this.fetchChunkInFragment(fragment, sampleIndex, options);
return this.fetchSampleInFragment(fragment, sampleIndex, options);
}
if (fragmentIndex !== -1) {
bestFragmentIndex = fragmentIndex;
@@ -2115,7 +2130,7 @@ abstract class IsobmffTrackBacking<Chunk extends EncodedVideoChunk | EncodedAudi
if (bestFragmentIndex !== -1) {
// If we finished looping but didn't find a perfect match, still return the best match we found
const fragment = this.internalTrack.fragments[bestFragmentIndex]!;
return this.fetchChunkInFragment(fragment, bestSampleIndex, options);
return this.fetchSampleInFragment(fragment, bestSampleIndex, options);
}
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;
constructor(internalTrack: InternalVideoTrack) {
@@ -2163,17 +2178,18 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking<EncodedVideoChunk> im
};
}
createChunk(data: Uint8Array, timestamp: number, duration: number, isKeyFrame: boolean) {
return new EncodedVideoChunk({
data,
timestamp,
duration,
type: isKeyFrame ? 'key' : 'delta',
});
createSample(
data: Uint8Array,
byteLength: number,
type: SampleType,
timestamp: number,
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;
constructor(internalTrack: InternalAudioTrack) {
@@ -2206,13 +2222,14 @@ class IsobmffAudioTrackBacking extends IsobmffTrackBacking<EncodedAudioChunk> im
};
}
createChunk(data: Uint8Array, timestamp: number, duration: number, isKeyFrame: boolean) {
return new EncodedAudioChunk({
data,
timestamp,
duration,
type: isKeyFrame ? 'key' : 'delta',
});
createSample(
data: Uint8Array,
byteLength: number,
type: SampleType,
timestamp: number,
duration: number,
) {
return new EncodedAudioSample(data, type, timestamp, duration, byteLength);
}
}
+368 -319
View File
File diff suppressed because it is too large Load Diff
+18 -4
View File
@@ -34,10 +34,24 @@ export class Reader {
return;
}
const encasingSegmentExists = this.loadedSegments.some(x => x.start <= start && x.end >= end);
if (encasingSegmentExists) {
// Nothing to load
return;
const index = binarySearchLessOrEqual(
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
return;
}
}
}
const bytesPromise = this.source._read(start, end);
+84
View File
@@ -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,
});
}
}