mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 13:53:53 +02:00
Add FilePathSource for server-side use
This commit is contained in:
+2
-2
@@ -8,11 +8,11 @@
|
|||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"main": "./dist/bundles/mediabunny.cjs",
|
"main": "./dist/bundles/mediabunny.cjs",
|
||||||
"module": "./dist/modules/src/index.js",
|
"module": "./dist/bundles/mediabunny.mjs",
|
||||||
"types": "./dist/modules/src/index.d.ts",
|
"types": "./dist/modules/src/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
"types": "./dist/modules/src/index.d.ts",
|
"types": "./dist/modules/src/index.d.ts",
|
||||||
"import": "./dist/modules/src/index.js",
|
"import": "./dist/bundles/mediabunny.mjs",
|
||||||
"require": "./dist/bundles/mediabunny.cjs"
|
"require": "./dist/bundles/mediabunny.cjs"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ export default async function inlineWorker(scriptText) {
|
|||||||
|
|
||||||
let Worker;
|
let Worker;
|
||||||
try {
|
try {
|
||||||
Worker = (await import('worker_threads')).Worker;
|
Worker = (await import('worker_threads')).Worker;
|
||||||
} catch {
|
} catch {
|
||||||
const workerModule = 'worker_threads';
|
const workerModule = 'worker_threads';
|
||||||
Worker = require(workerModule).Worker;
|
Worker = require(workerModule).Worker;
|
||||||
}
|
}
|
||||||
|
|
||||||
const worker = new Worker(scriptText, { eval: true });
|
const worker = new Worker(scriptText, { eval: true });
|
||||||
|
|||||||
+1
-1
@@ -1069,7 +1069,7 @@ export type Av1CodecInfo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Iterates over all OBUs in an AV1 packet bistream. */
|
/** Iterates over all OBUs in an AV1 packet bistream. */
|
||||||
export function* iterateAv1PacketObus(packet: Uint8Array) {
|
export const iterateAv1PacketObus = function* (packet: Uint8Array) {
|
||||||
// https://aomediacodec.github.io/av1-spec/av1-spec.pdf
|
// https://aomediacodec.github.io/av1-spec/av1-spec.pdf
|
||||||
|
|
||||||
const bitstream = new Bitstream(packet);
|
const bitstream = new Bitstream(packet);
|
||||||
|
|||||||
+4
-2
@@ -94,12 +94,14 @@ export { Rotation, AnyIterable, SetRequired, MaybePromise } from './misc';
|
|||||||
export {
|
export {
|
||||||
Source,
|
Source,
|
||||||
BufferSource,
|
BufferSource,
|
||||||
StreamSource,
|
|
||||||
StreamSourceOptions,
|
|
||||||
BlobSource,
|
BlobSource,
|
||||||
BlobSourceOptions,
|
BlobSourceOptions,
|
||||||
UrlSource,
|
UrlSource,
|
||||||
UrlSourceOptions,
|
UrlSourceOptions,
|
||||||
|
FilePathSource,
|
||||||
|
FilePathSourceOptions,
|
||||||
|
StreamSource,
|
||||||
|
StreamSourceOptions,
|
||||||
} from './source';
|
} from './source';
|
||||||
export {
|
export {
|
||||||
InputFormat,
|
InputFormat,
|
||||||
|
|||||||
+79
-5
@@ -6,6 +6,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { FileHandle } from 'node:fs/promises';
|
||||||
import {
|
import {
|
||||||
assert,
|
assert,
|
||||||
binarySearchLessOrEqual,
|
binarySearchLessOrEqual,
|
||||||
@@ -35,7 +36,7 @@ export abstract class Source {
|
|||||||
abstract _read(start: number, end: number): MaybePromise<ReadResult>;
|
abstract _read(start: number, end: number): MaybePromise<ReadResult>;
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_sizePromise: Promise<number> | null = null;
|
private _sizePromise: Promise<number> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves with the total size of the file in bytes. This function is memoized, meaning only the first call
|
* Resolves with the total size of the file in bytes. This function is memoized, meaning only the first call
|
||||||
@@ -250,7 +251,9 @@ export class UrlSource extends Source {
|
|||||||
|
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._url = url instanceof URL ? url : new URL(url, location.href);
|
this._url = url instanceof URL
|
||||||
|
? url
|
||||||
|
: new URL(url, typeof location !== 'undefined' ? location.href : undefined);
|
||||||
this._options = options;
|
this._options = options;
|
||||||
this._getRetryDelay = options.getRetryDelay ?? (previousAttempts => Math.min(2 ** (previousAttempts - 2), 8));
|
this._getRetryDelay = options.getRetryDelay ?? (previousAttempts => Math.min(2 ** (previousAttempts - 2), 8));
|
||||||
|
|
||||||
@@ -449,20 +452,91 @@ export class UrlSource extends Source {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for FilePathSource.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type FilePathSourceOptions = {
|
||||||
|
/** The maximum number of bytes the cache is allowed to hold in memory. Defaults to 8 MiB. */
|
||||||
|
maxCacheSize?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A source backed by a path to a file. Intended for server-side usage in Node, Bun, or Deno.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export class FilePathSource extends Source {
|
||||||
|
/** @internal */
|
||||||
|
_streamSource: StreamSource;
|
||||||
|
|
||||||
|
constructor(filePath: string, options: BlobSourceOptions = {}) {
|
||||||
|
if (typeof filePath !== 'string') {
|
||||||
|
throw new TypeError('filePath must be a string.');
|
||||||
|
}
|
||||||
|
if (!options || typeof options !== 'object') {
|
||||||
|
throw new TypeError('options must be an object.');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
options.maxCacheSize !== undefined
|
||||||
|
&& (!Number.isInteger(options.maxCacheSize) || options.maxCacheSize < 0)
|
||||||
|
) {
|
||||||
|
throw new TypeError('options.maxCacheSize, when provided, must be a non-negative integer.');
|
||||||
|
}
|
||||||
|
|
||||||
|
super();
|
||||||
|
|
||||||
|
let fileHandle: FileHandle | null = null;
|
||||||
|
|
||||||
|
// Let's back this source with a StreamSource, makes the implementation very simple
|
||||||
|
this._streamSource = new StreamSource({
|
||||||
|
getSize: async () => {
|
||||||
|
const FS_MODULE_NAME = 'node:fs/promises';
|
||||||
|
const fs = await import(FS_MODULE_NAME) as typeof import('node:fs/promises');
|
||||||
|
fileHandle = await fs.open(filePath, 'r');
|
||||||
|
|
||||||
|
const stats = await fileHandle.stat();
|
||||||
|
return stats.size;
|
||||||
|
},
|
||||||
|
read: async (start, end) => {
|
||||||
|
assert(fileHandle);
|
||||||
|
|
||||||
|
const buffer = Buffer.alloc(end - start);
|
||||||
|
await fileHandle.read(buffer, 0, end - start, start);
|
||||||
|
return buffer;
|
||||||
|
},
|
||||||
|
maxCacheSize: options.maxCacheSize,
|
||||||
|
prefetchProfile: 'fileSystem',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_read(start: number, end: number): MaybePromise<ReadResult> {
|
||||||
|
return this._streamSource._read(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_retrieveSize(): MaybePromise<number> {
|
||||||
|
return this._streamSource._retrieveSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for defining a StreamSource.
|
* Options for defining a StreamSource.
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export type StreamSourceOptions = {
|
export type StreamSourceOptions = {
|
||||||
|
/**
|
||||||
|
* Called when the size of the entire file is requested. Must return or resolve to the size in bytes. This function
|
||||||
|
* is guaranteed to be called before `read`.
|
||||||
|
*/
|
||||||
|
getSize: () => MaybePromise<number>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when data is requested. Must return or resolve to the bytes from the specified byte range, or a stream
|
* Called when data is requested. Must return or resolve to the bytes from the specified byte range, or a stream
|
||||||
* that yields these bytes.
|
* that yields these bytes.
|
||||||
*/
|
*/
|
||||||
read: (start: number, end: number) => MaybePromise<Uint8Array | ReadableStream<Uint8Array>>;
|
read: (start: number, end: number) => MaybePromise<Uint8Array | ReadableStream<Uint8Array>>;
|
||||||
|
|
||||||
/** Called when the size of the entire file is requested. Must return or resolve to the size in bytes. */
|
|
||||||
getSize: () => MaybePromise<number>;
|
|
||||||
|
|
||||||
/** The maximum number of bytes the cache is allowed to hold in memory. Defaults to 8 MiB. */
|
/** The maximum number of bytes the cache is allowed to hold in memory. Defaults to 8 MiB. */
|
||||||
maxCacheSize?: number;
|
maxCacheSize?: number;
|
||||||
|
|
||||||
|
|||||||
+2
-20
@@ -1,30 +1,12 @@
|
|||||||
import { expect, test } from 'vitest';
|
import { expect, test } from 'vitest';
|
||||||
import { ALL_FORMATS, MP4, StreamSource, EncodedPacketSink, Input } from '../src/index.js';
|
import { ALL_FORMATS, MP4, EncodedPacketSink, Input, FilePathSource } from '../src/index.js';
|
||||||
import { open } from 'node:fs/promises';
|
|
||||||
|
|
||||||
const __dirname = new URL('.', import.meta.url).pathname;
|
const __dirname = new URL('.', import.meta.url).pathname;
|
||||||
|
|
||||||
const fileHandle = await open(
|
|
||||||
`${__dirname}/files/video.mp4`,
|
|
||||||
'r',
|
|
||||||
);
|
|
||||||
|
|
||||||
const source = new StreamSource({
|
|
||||||
read: async (start, end) => {
|
|
||||||
const buffer = Buffer.alloc(end - start);
|
|
||||||
await fileHandle.read(buffer, 0, end - start, start);
|
|
||||||
return buffer;
|
|
||||||
},
|
|
||||||
getSize: async () => {
|
|
||||||
const { size } = await fileHandle.stat();
|
|
||||||
return size;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Should be able to get packets from a .MP4 file', async () => {
|
test('Should be able to get packets from a .MP4 file', async () => {
|
||||||
const input = new Input({
|
const input = new Input({
|
||||||
|
source: new FilePathSource(`${__dirname}/files/video.mp4`),
|
||||||
formats: ALL_FORMATS,
|
formats: ALL_FORMATS,
|
||||||
source,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await input.getFormat()).toBe(MP4);
|
expect(await input.getFormat()).toBe(MP4);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2021",
|
"target": "ES2021",
|
||||||
|
"module": "esnext",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user