Files
mediabunny/src/sample.ts
T
2025-02-04 16:59:25 +01:00

221 lines
5.8 KiB
TypeScript

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,
) {
if (!(data instanceof Uint8Array)) {
throw new TypeError('data must be a Uint8Array.');
}
if (type !== 'key' && type !== 'delta') {
throw new TypeError('type must be either "key" or "delta".');
}
if (!Number.isFinite(timestamp)) {
throw new TypeError('timestamp must be a number.');
}
if (!Number.isFinite(duration) || duration < 0) {
throw new TypeError('duration must be a non-negative number.');
}
if (!Number.isInteger(byteLength) || byteLength < 0) {
throw new TypeError('byteLength must be a non-negative integer.');
}
}
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,
});
}
is(otherSample: EncodedVideoSample) {
if (!(otherSample instanceof EncodedVideoSample)) {
throw new TypeError('otherSample must be an EncodedVideoSample.');
}
return (
this.type === otherSample.type
&& this.timestamp === otherSample.timestamp
&& this.duration === otherSample.duration
&& this.byteLength === otherSample.byteLength
);
}
clone(options?: {
timestamp?: number;
duration?: number;
}) {
if (options !== undefined && (!options || typeof options !== 'object')) {
throw new TypeError('options, when provided, must be an object.');
}
if (options?.timestamp !== undefined && !Number.isFinite(options.timestamp)) {
throw new TypeError('options.timestamp, when provided, must be a number.');
}
if (options?.duration !== undefined && !Number.isFinite(options.duration)) {
throw new TypeError('options.duration, when provided, must be a number.');
}
return new EncodedVideoSample(
this.data,
this.type,
options?.timestamp ?? this.timestamp,
options?.duration ?? this.duration,
this.byteLength,
);
}
static fromEncodedVideoChunk(chunk: EncodedVideoChunk) {
if (!(chunk instanceof EncodedVideoChunk)) {
throw new TypeError('chunk must be an EncodedVideoChunk.');
}
const data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
return new EncodedVideoSample(
data,
chunk.type,
chunk.timestamp / 1e6,
(chunk.duration ?? 0) / 1e6,
);
}
}
/** @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,
) {
if (!(data instanceof Uint8Array)) {
throw new TypeError('data must be a Uint8Array.');
}
if (type !== 'key' && type !== 'delta') {
throw new TypeError('type must be either "key" or "delta".');
}
if (!Number.isFinite(timestamp)) {
throw new TypeError('timestamp must be a number.');
}
if (!Number.isFinite(duration) || duration < 0) {
throw new TypeError('duration must be a non-negative number.');
}
if (!Number.isInteger(byteLength) || byteLength < 0) {
throw new TypeError('byteLength must be a non-negative integer.');
}
}
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,
});
}
is(otherSample: EncodedAudioSample) {
if (!(otherSample instanceof EncodedAudioSample)) {
throw new TypeError('otherSample must be an EncodedAudioSample.');
}
return (
this.type === otherSample.type
&& this.timestamp === otherSample.timestamp
&& this.duration === otherSample.duration
&& this.byteLength === otherSample.byteLength
);
}
clone(options?: {
timestamp?: number;
duration?: number;
}) {
if (options !== undefined && (!options || typeof options !== 'object')) {
throw new TypeError('options, when provided, must be an object.');
}
if (options?.timestamp !== undefined && !Number.isFinite(options.timestamp)) {
throw new TypeError('options.timestamp, when provided, must be a number.');
}
if (options?.duration !== undefined && !Number.isFinite(options.duration)) {
throw new TypeError('options.duration, when provided, must be a number.');
}
return new EncodedAudioSample(
this.data,
this.type,
options?.timestamp ?? this.timestamp,
options?.duration ?? this.duration,
this.byteLength,
);
}
static fromEncodedAudioChunk(chunk: EncodedAudioChunk) {
if (!(chunk instanceof EncodedAudioChunk)) {
throw new TypeError('chunk must be an EncodedAudioChunk.');
}
const data = new Uint8Array(chunk.byteLength);
chunk.copyTo(data);
return new EncodedAudioSample(
data,
chunk.type as SampleType, // Typing is weird
chunk.timestamp / 1e6,
(chunk.duration ?? 0) / 1e6,
);
}
}