forked from forks/ffprobe-wasm-npm
Add project files
This commit is contained in:
49
src/browser.mts
Normal file
49
src/browser.mts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { Chapter, ChapterTag, FileInfo, Frame, FramesInfo, Stream } from "./ffprobe-wasm.mjs";
|
||||
import type {
|
||||
IncomingMessage,
|
||||
IncomingData,
|
||||
OutgoingMessage,
|
||||
} from "./worker.mjs";
|
||||
|
||||
export class FFprobeWorker {
|
||||
readonly #worker: Worker;
|
||||
|
||||
constructor() {
|
||||
this.#worker = new Worker("./worker-browser.mjs");
|
||||
}
|
||||
|
||||
async getFileInfo(file: File): Promise<FileInfo> {
|
||||
return this.#postMessage({ type: "getFileInfo", payload: [file.name, { files: [file] }] });
|
||||
}
|
||||
|
||||
async getFrames(file: File, offset: number): Promise<FramesInfo> {
|
||||
return this.#postMessage({ type: "getFrames", payload: [file.name, { files: [file] }, offset] });
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
this.#worker.terminate();
|
||||
}
|
||||
|
||||
#postMessage(data: IncomingData): Promise<any> {
|
||||
const channel = new MessageChannel();
|
||||
const message: IncomingMessage = {
|
||||
...data,
|
||||
port: channel.port2
|
||||
};
|
||||
|
||||
this.#worker.postMessage(message, [channel.port2]);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
channel.port1.onmessage = (event: MessageEvent<OutgoingMessage>) => {
|
||||
const { data } = event;
|
||||
if (data.status === "success") {
|
||||
resolve(data.payload);
|
||||
} else {
|
||||
reject(new Error(data.message));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type { Chapter, ChapterTag, FileInfo, Frame, FramesInfo, Stream };
|
||||
145
src/ffprobe-wasm-shared.d.ts
vendored
Normal file
145
src/ffprobe-wasm-shared.d.ts
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
export interface FFprobe {
|
||||
get_file_info(path: string): Raw<FileInfo>;
|
||||
get_frames(path: string, offset: number): Raw<FramesInfo>;
|
||||
FS: FS;
|
||||
avutil_version(): string;
|
||||
avcodec_version(): string;
|
||||
avformat_version(): string;
|
||||
onRuntimeInitialized(): void;
|
||||
}
|
||||
|
||||
export type FSMountOptions = WorkerFSMountOptions | NodeFSMountOptions;
|
||||
|
||||
export interface WorkerFSMountOptions {
|
||||
files?: File[];
|
||||
blobs?: Blob[];
|
||||
packages?: any[];
|
||||
}
|
||||
|
||||
export interface NodeFSMountOptions {
|
||||
root: string;
|
||||
}
|
||||
|
||||
export interface FSFilesystemMountOptions {
|
||||
type: FSFilesystem;
|
||||
opts: FSFilesystemMountOptions;
|
||||
mountpoint: string;
|
||||
mounts: any[];
|
||||
}
|
||||
|
||||
export interface FSFilesystem {
|
||||
mount(opts: FSFilesystemMountOptions): FSNode;
|
||||
}
|
||||
|
||||
export interface FSNode {
|
||||
contents: Record<string, FSNode>;
|
||||
id: number;
|
||||
mode: number;
|
||||
name: string;
|
||||
parent: FSNode;
|
||||
timestamp: number;
|
||||
isDevice(): boolean;
|
||||
isFolder(): boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface FSFilesystems {
|
||||
MEMFS: FSFilesystem;
|
||||
WORKERFS: FSFilesystem;
|
||||
NODEFS: FSFilesystem;
|
||||
}
|
||||
|
||||
export interface FS {
|
||||
analyzePath(path: string, dontResolveLastLink?: boolean): AnalyzePathReturn;
|
||||
mkdir(path: string, mode?: number): number;
|
||||
mount(type: FSFilesystem, opts: FSMountOptions, mountpoint: string): FSNode;
|
||||
unmount(mountpoint: string): void;
|
||||
filesystems: FSFilesystems;
|
||||
}
|
||||
|
||||
export interface AnalyzePathReturn {
|
||||
isRoot: boolean;
|
||||
exists: boolean;
|
||||
error: number;
|
||||
name: string | null;
|
||||
path: string | null;
|
||||
object: any | null;
|
||||
parentExists: boolean;
|
||||
parentPath: string | null;
|
||||
parentObject: any | null;
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
bit_rate: number
|
||||
chapters: Chapter[]
|
||||
duration: number
|
||||
flags: number
|
||||
name: string
|
||||
nb_chapters: number
|
||||
nb_streams: number
|
||||
streams: Stream[]
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface Collection<T> {
|
||||
count: { value: number }
|
||||
ptr: number;
|
||||
ptrType: any;
|
||||
get(index: number): T;
|
||||
size(): number;
|
||||
}
|
||||
|
||||
export type Raw<T> = {
|
||||
[K in keyof T]: T[K] extends Array<infer U> ? Collection<Raw<U>> : T[K]
|
||||
}
|
||||
|
||||
export interface Chapter {
|
||||
end: number
|
||||
id: number
|
||||
start: number
|
||||
tags: ChapterTag[]
|
||||
/**
|
||||
* @example "1/1000"
|
||||
*/
|
||||
time_base: string
|
||||
}
|
||||
|
||||
export interface ChapterTag {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface Stream {
|
||||
bit_rate: number
|
||||
channels: number
|
||||
codec_name: string
|
||||
codec_type: number
|
||||
duration: number
|
||||
format: string
|
||||
frame_size: number
|
||||
height: number
|
||||
id: number
|
||||
level: number
|
||||
profile: string
|
||||
sample_rate: number
|
||||
start_time: number
|
||||
width: number
|
||||
}
|
||||
|
||||
export interface FramesInfo {
|
||||
avg_frame_rate: number
|
||||
duration: number
|
||||
frames: Frame[]
|
||||
gop_size: number
|
||||
nb_frames: number
|
||||
time_base: number
|
||||
}
|
||||
|
||||
export interface Frame {
|
||||
dts: number
|
||||
frame_number: number
|
||||
pict_type: number
|
||||
pkt_size: number
|
||||
pos: number
|
||||
pts: number
|
||||
}
|
||||
9
src/ffprobe-wasm.d.mts
Normal file
9
src/ffprobe-wasm.d.mts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { FFprobe } from "./ffprobe-wasm-shared";
|
||||
|
||||
export * from "./ffprobe-wasm-shared";
|
||||
|
||||
export default function loadFFprobe(ffprobe?: FFprobeInit): Promise<FFprobe>;
|
||||
|
||||
export interface FFprobeInit {
|
||||
locateFile?(path: string, scriptDirectory: string): string;
|
||||
}
|
||||
7
src/ffprobe-wasm.d.ts
vendored
Normal file
7
src/ffprobe-wasm.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import { FFprobe } from "./ffprobe-wasm-shared";
|
||||
|
||||
export * from "./ffprobe-wasm-shared";
|
||||
|
||||
declare const ffprobe: FFprobe;
|
||||
|
||||
export default ffprobe;
|
||||
52
src/node.mts
Normal file
52
src/node.mts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { basename, dirname } from "path";
|
||||
import { fileURLToPath } from 'url';
|
||||
import { MessageChannel, Worker } from "worker_threads";
|
||||
import type { Chapter, ChapterTag, FileInfo, Frame, FramesInfo, Stream } from "./ffprobe-wasm.mjs";
|
||||
import type {
|
||||
IncomingMessage,
|
||||
IncomingData,
|
||||
OutgoingMessage,
|
||||
} from "./worker.mjs";
|
||||
|
||||
export class FFprobeWorker {
|
||||
readonly #worker: Worker;
|
||||
|
||||
constructor() {
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
this.#worker = new Worker(`${__dirname}/worker-node.mjs`);
|
||||
}
|
||||
|
||||
async getFileInfo(filePath: string): Promise<FileInfo> {
|
||||
return this.#postMessage({ type: "getFileInfo", payload: [basename(filePath), { root: dirname(filePath) }] });
|
||||
}
|
||||
|
||||
async getFrames(filePath: string, offset: number): Promise<FramesInfo> {
|
||||
return this.#postMessage({ type: "getFrames", payload: [basename(filePath), { root: dirname(filePath) }, offset] });
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
this.#worker.terminate();
|
||||
}
|
||||
|
||||
#postMessage(data: IncomingData): Promise<any> {
|
||||
const channel = new MessageChannel();
|
||||
const message: IncomingMessage = {
|
||||
...data,
|
||||
port: channel.port2
|
||||
};
|
||||
|
||||
this.#worker.postMessage(message, [channel.port2]);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
channel.port1.on("message", (data: OutgoingMessage) => {
|
||||
if (data.status === "success") {
|
||||
resolve(data.payload);
|
||||
} else {
|
||||
reject(new Error(data.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type { Chapter, ChapterTag, FileInfo, Frame, FramesInfo, Stream };
|
||||
11
src/worker-browser.mts
Normal file
11
src/worker-browser.mts
Normal file
@@ -0,0 +1,11 @@
|
||||
import loadFFprobe from "./ffprobe-wasm.mjs";
|
||||
import { createListener, IncomingMessage } from "./worker.mjs";
|
||||
|
||||
const listener = createListener(
|
||||
loadFFprobe({
|
||||
locateFile: (path) => `${location.origin}/node_modules/ffprobe-wasm/${path}`
|
||||
}),
|
||||
"WORKERFS",
|
||||
);
|
||||
|
||||
self.onmessage = (event: MessageEvent<IncomingMessage>) => listener(event.data);
|
||||
19
src/worker-node.mts
Normal file
19
src/worker-node.mts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createRequire } from "module";
|
||||
import { parentPort } from "worker_threads";
|
||||
import type { FFprobe } from "./ffprobe-wasm.js";
|
||||
import { createListener } from "./worker.mjs";
|
||||
|
||||
if (!parentPort) {
|
||||
throw new Error("parentPort must be defined. Are you sure you are in a worker context?");
|
||||
}
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const listener = createListener(new Promise((resolve) => {
|
||||
const ffprobe: FFprobe = require('./ffprobe-wasm.js')
|
||||
ffprobe.onRuntimeInitialized = () => {
|
||||
resolve(ffprobe);
|
||||
}
|
||||
}), "NODEFS");
|
||||
|
||||
parentPort.on("message", listener);
|
||||
133
src/worker.mts
Normal file
133
src/worker.mts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { MessagePort as NodeMessagePort } from "worker_threads";
|
||||
import type { Chapter, ChapterTag, FFprobe, FileInfo, FramesInfo, FSFilesystems, FSMountOptions, Stream } from "./ffprobe-wasm-shared";
|
||||
|
||||
export type IncomingMessage = { port: MessagePort | NodeMessagePort } & IncomingData;
|
||||
|
||||
export type IncomingData =
|
||||
| {
|
||||
type: "getFileInfo";
|
||||
payload: [fileName: string, mountOptions: FSMountOptions];
|
||||
}
|
||||
| {
|
||||
type: "getFrames";
|
||||
payload: [fileName: string, mountOptions: FSMountOptions, offset: number];
|
||||
};
|
||||
|
||||
export type OutgoingMessage =
|
||||
| ({ status: "success" } & OutgoingData)
|
||||
| { status: "error"; message: string };
|
||||
|
||||
export type OutgoingData =
|
||||
| {
|
||||
type: "getFileInfo";
|
||||
payload: FileInfo;
|
||||
}
|
||||
| {
|
||||
type: "getFrames";
|
||||
payload: FramesInfo;
|
||||
};
|
||||
|
||||
export function createListener(
|
||||
ffprobePromise: Promise<FFprobe>,
|
||||
fsType: keyof FSFilesystems,
|
||||
) {
|
||||
return onmessage;
|
||||
|
||||
async function onmessage(data: IncomingMessage) {
|
||||
try {
|
||||
switch (data.type) {
|
||||
case "getFileInfo":
|
||||
data.port.postMessage({
|
||||
status: "success",
|
||||
payload: await getFileInfo(...data.payload),
|
||||
type: data.type,
|
||||
});
|
||||
break;
|
||||
case "getFrames":
|
||||
data.port.postMessage({
|
||||
status: "success",
|
||||
payload: await getFrames(...data.payload),
|
||||
type: data.type,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
const _: never = data;
|
||||
throw new Error(`Unknown event: ${JSON.stringify(_)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
data.port.postMessage({
|
||||
status: "error",
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function getFileInfo(fileName: string, mountOptions: FSMountOptions): Promise<FileInfo> {
|
||||
const { FS, get_file_info } = await ffprobePromise;
|
||||
try {
|
||||
if (!FS.analyzePath("/work").exists) {
|
||||
FS.mkdir("/work");
|
||||
}
|
||||
FS.mount(FS.filesystems[fsType], mountOptions, "/work");
|
||||
|
||||
// Call the wasm module.
|
||||
const rawInfo = get_file_info(`/work/${fileName}`);
|
||||
|
||||
// Remap streams into collection.
|
||||
const streams: Stream[] = [];
|
||||
for (let i = 0; i < rawInfo.streams.size(); i++) {
|
||||
streams.push(rawInfo.streams.get(i));
|
||||
}
|
||||
|
||||
// Remap chapters into collection.
|
||||
const chapters: Chapter[] = [];
|
||||
for (let i = 0; i < rawInfo.chapters.size(); i++) {
|
||||
const rawChapter = rawInfo.chapters.get(i);
|
||||
|
||||
const tags: ChapterTag[] = [];
|
||||
for (let j = 0; j < rawChapter.tags.size(); j++) {
|
||||
tags.push(rawChapter.tags.get(j));
|
||||
}
|
||||
|
||||
chapters.push({ ...rawChapter, tags });
|
||||
}
|
||||
|
||||
return {
|
||||
...rawInfo,
|
||||
streams,
|
||||
chapters,
|
||||
};
|
||||
} finally {
|
||||
// Cleanup mount.
|
||||
FS.unmount("/work");
|
||||
}
|
||||
}
|
||||
|
||||
async function getFrames(fileName: string, mountOptions: FSMountOptions, offset: number): Promise<FramesInfo> {
|
||||
const { FS, get_frames } = await ffprobePromise;
|
||||
try {
|
||||
if (!FS.analyzePath("/work").exists) {
|
||||
FS.mkdir("/work");
|
||||
}
|
||||
FS.mount(FS.filesystems.WORKERFS, mountOptions, "/work");
|
||||
|
||||
// Call the wasm module.
|
||||
const framesInfo = get_frames(`/work/${fileName}`, offset);
|
||||
|
||||
// Remap frames into collection.
|
||||
const frames = [];
|
||||
for (let i = 0; i < framesInfo.frames.size(); i++) {
|
||||
frames.push(framesInfo.frames.get(i));
|
||||
}
|
||||
|
||||
return {
|
||||
...framesInfo,
|
||||
frames,
|
||||
};
|
||||
} finally {
|
||||
// Cleanup mount.
|
||||
FS.unmount("/work");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user