Add project files

This commit is contained in:
Tomás Fox
2022-03-06 22:46:03 -03:00
commit 6f73cd3bdb
17 changed files with 613 additions and 0 deletions

49
src/browser.mts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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");
}
}
}