mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-29 03:43:50 +02:00
Merge branch 'main' into cursors
This commit is contained in:
+40
-1
@@ -22,6 +22,7 @@ import {
|
||||
VideoCodec,
|
||||
} from './codec';
|
||||
import { customAudioEncoders, customVideoEncoders } from './custom-coder';
|
||||
import { isFirefox } from './misc';
|
||||
import { EncodedPacket } from './packet';
|
||||
|
||||
/**
|
||||
@@ -529,7 +530,45 @@ export const canEncodeVideo = async (
|
||||
});
|
||||
|
||||
const support = await VideoEncoder.isConfigSupported(encoderConfig);
|
||||
return support.supported === true;
|
||||
if (!support.supported) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isFirefox()) {
|
||||
// isConfigSupported on Firefox appears to unreliably indicate if encoding will actually succeed. Therefore, we
|
||||
// just try encoding a frame to see if it actually works.
|
||||
// https://github.com/Vanilagy/mediabunny/issues/222
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor
|
||||
return new Promise<boolean>(async (resolve) => {
|
||||
try {
|
||||
const encoder = new VideoEncoder({
|
||||
output: () => {},
|
||||
error: () => resolve(false),
|
||||
});
|
||||
encoder.configure(encoderConfig);
|
||||
|
||||
const frameData = new Uint8Array(width * height * 4);
|
||||
const frame = new VideoFrame(frameData, {
|
||||
format: 'RGBA',
|
||||
codedWidth: width,
|
||||
codedHeight: height,
|
||||
timestamp: 0,
|
||||
});
|
||||
|
||||
encoder.encode(frame);
|
||||
frame.close();
|
||||
|
||||
await encoder.flush();
|
||||
|
||||
resolve(true);
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -850,7 +850,7 @@ export const dOps = (trackData: IsobmffAudioTrackData) => {
|
||||
let inputSampleRate = trackData.info.sampleRate;
|
||||
let outputGain = 0;
|
||||
let channelMappingFamily = 0;
|
||||
let channelMappingTable = new Uint8Array(0);
|
||||
let channelMappingTable: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
|
||||
|
||||
// Read preskip and from codec private data from the encoder
|
||||
// https://www.rfc-editor.org/rfc/rfc7845#section-5
|
||||
|
||||
@@ -467,7 +467,7 @@ export class EBMLWriter {
|
||||
|
||||
export const MAX_VAR_INT_SIZE = 8;
|
||||
export const MIN_HEADER_SIZE = 2; // 1-byte ID and 1-byte size
|
||||
export const MAX_HEADER_SIZE = /* #__PURE__ */ 2 * MAX_VAR_INT_SIZE; // 8-byte ID and 8-byte size
|
||||
export const MAX_HEADER_SIZE = 2 * MAX_VAR_INT_SIZE; // 8-byte ID and 8-byte size
|
||||
|
||||
export const readVarIntSize = (slice: FileSlice) => {
|
||||
const firstByte = readU8(slice);
|
||||
|
||||
@@ -228,7 +228,7 @@ const METADATA_ELEMENTS = [
|
||||
{ id: EBMLId.Tracks, flag: 'tracksSeen' },
|
||||
{ id: EBMLId.Cues, flag: 'cuesSeen' },
|
||||
] as const;
|
||||
const MAX_RESYNC_LENGTH = /* #__PURE__ */ 10 * 2 ** 20; // 10 MiB
|
||||
const MAX_RESYNC_LENGTH = 10 * 2 ** 20; // 10 MiB
|
||||
|
||||
export class MatroskaDemuxer extends Demuxer {
|
||||
reader: Reader;
|
||||
|
||||
@@ -65,8 +65,8 @@ import { EncodedPacket } from '../packet';
|
||||
import { parseOpusIdentificationHeader } from '../codec-data';
|
||||
import { AttachedFile } from '../metadata';
|
||||
|
||||
const MIN_CLUSTER_TIMESTAMP_MS = /* #__PURE__ */ -(2 ** 15);
|
||||
const MAX_CLUSTER_TIMESTAMP_MS = /* #__PURE__ */ 2 ** 15 - 1;
|
||||
const MIN_CLUSTER_TIMESTAMP_MS = -(2 ** 15);
|
||||
const MAX_CLUSTER_TIMESTAMP_MS = 2 ** 15 - 1;
|
||||
const APP_NAME = 'Mediabunny';
|
||||
const SEGMENT_SIZE_BYTES = 6;
|
||||
const CLUSTER_SIZE_BYTES = 5;
|
||||
|
||||
+17
-8
@@ -157,20 +157,20 @@ export const writeBits = (bytes: Uint8Array, start: number, end: number, value:
|
||||
export const toUint8Array = (source: AllowSharedBufferSource): Uint8Array => {
|
||||
if (source.constructor === Uint8Array) { // We want a true Uint8Array, not something that extends it like Buffer
|
||||
return source;
|
||||
} else if (source instanceof ArrayBuffer) {
|
||||
return new Uint8Array(source);
|
||||
} else {
|
||||
} else if (ArrayBuffer.isView(source)) {
|
||||
return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
|
||||
} else {
|
||||
return new Uint8Array(source);
|
||||
}
|
||||
};
|
||||
|
||||
export const toDataView = (source: AllowSharedBufferSource) => {
|
||||
export const toDataView = (source: AllowSharedBufferSource): DataView => {
|
||||
if (source.constructor === DataView) {
|
||||
return source;
|
||||
} else if (source instanceof ArrayBuffer) {
|
||||
return new DataView(source);
|
||||
} else {
|
||||
} else if (ArrayBuffer.isView(source)) {
|
||||
return new DataView(source.buffer, source.byteOffset, source.byteLength);
|
||||
} else {
|
||||
return new DataView(source);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -515,7 +515,7 @@ export const isIso639Dash2LanguageCode = (x: string) => {
|
||||
};
|
||||
|
||||
// Since the result will be truncated, add a bit of eps to compensate for floating point errors
|
||||
export const SECOND_TO_MICROSECOND_FACTOR = /* #__PURE__ */ 1e6 * (1 + Number.EPSILON);
|
||||
export const SECOND_TO_MICROSECOND_FACTOR = 1e6 * (1 + Number.EPSILON);
|
||||
|
||||
/**
|
||||
* Sets all keys K of T to be required.
|
||||
@@ -584,6 +584,7 @@ export const retriedFetch = async (
|
||||
url: string | URL | Request,
|
||||
requestInit: RequestInit,
|
||||
getRetryDelay: (previousAttempts: number, error: unknown, url: string | URL | Request) => number | null,
|
||||
shouldStop: () => boolean,
|
||||
) => {
|
||||
let attempts = 0;
|
||||
|
||||
@@ -591,6 +592,10 @@ export const retriedFetch = async (
|
||||
try {
|
||||
return await fetchFn(url, requestInit);
|
||||
} catch (error) {
|
||||
if (shouldStop()) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
const retryDelayInSeconds = getRetryDelay(attempts, error, url);
|
||||
|
||||
@@ -607,6 +612,10 @@ export const retriedFetch = async (
|
||||
if (retryDelayInSeconds > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * retryDelayInSeconds));
|
||||
}
|
||||
|
||||
if (shouldStop()) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import { OGGS } from './ogg-misc';
|
||||
|
||||
export const MIN_PAGE_HEADER_SIZE = 27;
|
||||
export const MAX_PAGE_HEADER_SIZE = 27 + 255;
|
||||
export const MAX_PAGE_SIZE = /* #__PURE__ */ MAX_PAGE_HEADER_SIZE + 255 * 255;
|
||||
export const MAX_PAGE_SIZE = MAX_PAGE_HEADER_SIZE + 255 * 255;
|
||||
|
||||
export type Page = {
|
||||
headerStartPos: number;
|
||||
|
||||
+3
-1
@@ -1250,7 +1250,9 @@ export class AudioSample implements Disposable {
|
||||
numberOfFrames: this.numberOfFrames,
|
||||
numberOfChannels: this.numberOfChannels,
|
||||
timestamp: this.microsecondTimestamp,
|
||||
data: this._data,
|
||||
data: this._data.buffer instanceof ArrayBuffer
|
||||
? this._data.buffer
|
||||
: this._data.slice(), // In the case of SharedArrayBuffer, convert to ArrayBuffer
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+45
-41
@@ -102,10 +102,17 @@ export class BufferSource extends Source {
|
||||
/** @internal */
|
||||
_onreadCalled = false;
|
||||
|
||||
/** Creates a new {@link BufferSource} backed the specified `ArrayBuffer` or `ArrayBufferView`. */
|
||||
constructor(buffer: ArrayBuffer | ArrayBufferView) {
|
||||
if (!(buffer instanceof ArrayBuffer) && !ArrayBuffer.isView(buffer)) {
|
||||
throw new TypeError('buffer must be an ArrayBuffer or ArrayBufferView.');
|
||||
/**
|
||||
* Creates a new {@link BufferSource} backed by the specified `ArrayBuffer`, `SharedArrayBuffer`,
|
||||
* or `ArrayBufferView`.
|
||||
*/
|
||||
constructor(buffer: AllowSharedBufferSource) {
|
||||
if (
|
||||
!(buffer instanceof ArrayBuffer)
|
||||
&& !(typeof SharedArrayBuffer !== 'undefined' && buffer instanceof SharedArrayBuffer)
|
||||
&& !ArrayBuffer.isView(buffer)
|
||||
) {
|
||||
throw new TypeError('buffer must be an ArrayBuffer, SharedArrayBuffer, or ArrayBufferView.');
|
||||
}
|
||||
|
||||
super();
|
||||
@@ -234,12 +241,7 @@ export class BlobSource extends Source {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
this._orchestrator.forgetWorker(worker);
|
||||
|
||||
if (worker.currentPos < worker.targetPos) { // I think this `if` should always hit?
|
||||
throw new Error('Blob reader stopped unexpectedly before all requested data was read.');
|
||||
}
|
||||
|
||||
break;
|
||||
throw new Error('Blob reader stopped unexpectedly before all requested data was read.');
|
||||
}
|
||||
|
||||
if (worker.aborted) {
|
||||
@@ -269,7 +271,7 @@ export class BlobSource extends Source {
|
||||
}
|
||||
}
|
||||
|
||||
const URL_SOURCE_MIN_LOAD_AMOUNT = /* #__PURE__ */ 0.5 * 2 ** 20; // 0.5 MiB
|
||||
const URL_SOURCE_MIN_LOAD_AMOUNT = 0.5 * 2 ** 20; // 0.5 MiB
|
||||
const DEFAULT_RETRY_DELAY
|
||||
= ((previousAttempts, error, src) => {
|
||||
// Check if this could be a CORS error. If so, we cannot recover from it and
|
||||
@@ -297,6 +299,10 @@ const DEFAULT_RETRY_DELAY
|
||||
= typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean' ? navigator.onLine : true;
|
||||
|
||||
if (isOnline && originOfSrc !== null && originOfSrc !== window.location.origin) {
|
||||
console.warn(
|
||||
`Request will not be retried because a CORS error was suspected due to different origins. You can`
|
||||
+ ` modify this behavior by providing your own function for the 'getRetryDelay' option.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -425,6 +431,7 @@ export class UrlSource extends Source {
|
||||
signal: abortController.signal,
|
||||
}),
|
||||
this._getRetryDelay,
|
||||
() => this._disposed,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -436,7 +443,7 @@ export class UrlSource extends Source {
|
||||
let fileSize: number;
|
||||
|
||||
if (response.status === 206) {
|
||||
fileSize = this._getPartialLengthFromRangeResponse(response);
|
||||
fileSize = this._getTotalLengthFromRangeResponse(response);
|
||||
worker = this._orchestrator.createWorker(0, Math.min(fileSize, URL_SOURCE_MIN_LOAD_AMOUNT));
|
||||
} else {
|
||||
// Server probably returned a 200.
|
||||
@@ -492,6 +499,7 @@ export class UrlSource extends Source {
|
||||
signal: abortController.signal,
|
||||
}),
|
||||
this._getRetryDelay,
|
||||
() => this._disposed,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -509,14 +517,6 @@ export class UrlSource extends Source {
|
||||
);
|
||||
}
|
||||
|
||||
const length = this._getPartialLengthFromRangeResponse(response);
|
||||
const required = worker.targetPos - worker.currentPos;
|
||||
if (length < required) {
|
||||
throw new Error(
|
||||
`HTTP response unexpectedly too short: Needed at least ${required} bytes, got only ${length}.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error(
|
||||
'Missing HTTP response body stream. The used fetch function must provide the response body as a'
|
||||
@@ -539,6 +539,11 @@ export class UrlSource extends Source {
|
||||
try {
|
||||
readResult = await reader.read();
|
||||
} catch (error) {
|
||||
if (this._disposed) {
|
||||
// No need to try to retry
|
||||
throw error;
|
||||
}
|
||||
|
||||
const retryDelayInSeconds = this._getRetryDelay(1, error, this._url);
|
||||
if (retryDelayInSeconds !== null) {
|
||||
console.error('Error while reading response stream. Attempting to resume.', error);
|
||||
@@ -557,16 +562,17 @@ export class UrlSource extends Source {
|
||||
const { done, value } = readResult;
|
||||
|
||||
if (done) {
|
||||
this._orchestrator.forgetWorker(worker);
|
||||
|
||||
if (worker.currentPos < worker.targetPos) {
|
||||
throw new Error(
|
||||
'Response stream reader stopped unexpectedly before all requested data was read.',
|
||||
);
|
||||
if (worker.currentPos >= worker.targetPos) {
|
||||
// All data was delivered, we're good
|
||||
this._orchestrator.forgetWorker(worker);
|
||||
worker.running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
worker.running = false;
|
||||
return;
|
||||
// The response stopped early, before the target. This can happen if server decides to cap range
|
||||
// requests arbitrarily, even if the request had an uncapped end. In this case, let's fetch the rest
|
||||
// of the data using a new request.
|
||||
break;
|
||||
}
|
||||
|
||||
this.onread?.(worker.currentPos, worker.currentPos + value.length);
|
||||
@@ -586,25 +592,23 @@ export class UrlSource extends Source {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _getPartialLengthFromRangeResponse(response: Response) {
|
||||
private _getTotalLengthFromRangeResponse(response: Response) {
|
||||
const contentRange = response.headers.get('Content-Range');
|
||||
if (contentRange) {
|
||||
const match = /\/(\d+)/.exec(contentRange);
|
||||
if (match) {
|
||||
return Number(match[1]);
|
||||
} else {
|
||||
throw new Error(`Invalid Content-Range header: ${contentRange}`);
|
||||
}
|
||||
}
|
||||
|
||||
const contentLength = response.headers.get('Content-Length');
|
||||
if (contentLength) {
|
||||
return Number(contentLength);
|
||||
} else {
|
||||
const contentLength = response.headers.get('Content-Length');
|
||||
if (contentLength) {
|
||||
return Number(contentLength);
|
||||
} else {
|
||||
throw new Error(
|
||||
'Partial HTTP response (status 206) must surface either Content-Range or'
|
||||
+ ' Content-Length header.',
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
'Partial HTTP response (status 206) must surface either Content-Range or'
|
||||
+ ' Content-Length header.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,7 +643,7 @@ export class FilePathSource extends Source {
|
||||
_fileHandle: FileHandle | null = null;
|
||||
|
||||
/** Creates a new {@link FilePathSource} backed by the file at the specified file path. */
|
||||
constructor(filePath: string, options: BlobSourceOptions = {}) {
|
||||
constructor(filePath: string, options: FilePathSourceOptions = {}) {
|
||||
if (typeof filePath !== 'string') {
|
||||
throw new TypeError('filePath must be a string.');
|
||||
}
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ export type StreamTargetChunk = {
|
||||
/** The operation type. */
|
||||
type: 'write'; // This ensures automatic compatibility with FileSystemWritableFileStream
|
||||
/** The data to write. */
|
||||
data: Uint8Array;
|
||||
data: Uint8Array<ArrayBuffer>;
|
||||
/** The byte offset in the output file at which to write the data. */
|
||||
position: number;
|
||||
};
|
||||
|
||||
+5
-5
@@ -89,8 +89,8 @@ export abstract class Writer {
|
||||
}
|
||||
}
|
||||
|
||||
const ARRAY_BUFFER_INITIAL_SIZE = /* #__PURE__ */ 2 ** 16;
|
||||
const ARRAY_BUFFER_MAX_SIZE = /* #__PURE__ */ 2 ** 32;
|
||||
const ARRAY_BUFFER_INITIAL_SIZE = 2 ** 16;
|
||||
const ARRAY_BUFFER_MAX_SIZE = 2 ** 32;
|
||||
|
||||
export class BufferTargetWriter extends Writer {
|
||||
private pos = 0;
|
||||
@@ -184,13 +184,13 @@ export class BufferTargetWriter extends Writer {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_CHUNK_SIZE = /* #__PURE__ */ 2 ** 24;
|
||||
const DEFAULT_CHUNK_SIZE = 2 ** 24;
|
||||
const MAX_CHUNKS_AT_ONCE = 2;
|
||||
|
||||
interface Chunk {
|
||||
start: number;
|
||||
written: ChunkSection[];
|
||||
data: Uint8Array;
|
||||
data: Uint8Array<ArrayBuffer>;
|
||||
shouldFlush: boolean;
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ export class StreamTargetWriter extends Writer {
|
||||
const chunks: {
|
||||
start: number;
|
||||
size: number;
|
||||
data?: Uint8Array;
|
||||
data?: Uint8Array<ArrayBuffer>;
|
||||
}[] = [];
|
||||
const sorted = [...this.sections].sort((a, b) => a.start - b.start);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user