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

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.DS_Store
.vscode/
node_modules/
dist/

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "ffprobe-wasm-app"]
path = ffprobe-wasm-app
url = https://github.com/alfg/ffprobe-wasm

22
LICENSE Normal file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2022 Tomás Fox
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

42
README.md Normal file
View File

@@ -0,0 +1,42 @@
ffprobe-wasm
==========
Gather information from multimedia streams. Works on the browser and Node.js.
Uses the code at [alfg/ffprobe-wasm](https://github.com/alfg/ffprobe-wasm), but in a packaged format, so it can be reused in other projects.
_Note_: This project doesn't build or use FFProbe. Instead it uses FFmpeg's libavformat and libavcodec to output similar results.
## Installation
```sh
npm install ffprobe-wasm
```
## Examples
Node.js
```ts
import { FFprobeWorker } from 'ffprobe-wasm/node.mjs';
const worker = new FFprobeWorker();
const fileInfo = await worker.getFileInfo('file.mp4');
console.log(fileInfo);
```
Browser
```ts
import { FFprobeWorker } from 'ffprobe-wasm/browser.mjs';
const worker = new FFprobeWorker();
// input is the reference to a <input type="file" /> element
input.addEventListener('change', (event) => {
const file = event.target.files[0]
const fileInfo = await worker.getFileInfo(file);
console.log(fileInfo);
});
```

22
build.sh Executable file
View File

@@ -0,0 +1,22 @@
#/bin/sh
# Exit on error
set -e
# Clean
rm -rf dist
rm -rf ffprobe-wasm-app/dist
# Build wasm
cd ffprobe-wasm-app
# sed -i -e 's/ffprobe-wasm\.js/ffprobe-wasm.mjs/g;s/-o dist\/ffprobe-wasm\.mjs \\/-o dist\/ffprobe-wasm.mjs -s EXPORT_NAME=ffprobe \\/' Makefile
docker-compose run ffprobe-wasm make
cd ..
cp -R ffprobe-wasm-app/dist dist
cp src/*.d.* dist
# Build browser/node workers
npm run build
# Copy package.json
cp package.json dist

1
ffprobe-wasm-app Submodule

Submodule ffprobe-wasm-app added at ff36b01373

50
package-lock.json generated Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "ffprobe-wasm",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ffprobe-wasm",
"version": "0.1.0",
"license": "MIT",
"devDependencies": {
"@types/node": "^17.0.21",
"typescript": "^4.5.5"
}
},
"node_modules/@types/node": {
"version": "17.0.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.21.tgz",
"integrity": "sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==",
"dev": true
},
"node_modules/typescript": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz",
"integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
}
},
"dependencies": {
"@types/node": {
"version": "17.0.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.21.tgz",
"integrity": "sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==",
"dev": true
},
"typescript": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz",
"integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==",
"dev": true
}
}
}

27
package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "ffprobe-wasm",
"version": "0.1.0",
"description": "ffprobe-like for browser and node, powered by WebAssembly",
"repository": {
"type": "git",
"url": "https://github.com/tfoxy/ffprobe-wasm"
},
"bugs": {
"url": "https://github.com/tfoxy/ffprobe-wasm/issues"
},
"keywords": [
"ffprobe",
"WebAssembly",
"ffmpeg",
"video"
],
"author": "Tomás Fox <tomas.c.fox@gmail.com>",
"license": "MIT",
"scripts": {
"build": "tsc"
},
"devDependencies": {
"@types/node": "^17.0.21",
"typescript": "^4.5.5"
}
}

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");
}
}
}

17
tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2021",
"lib": ["DOM", "ES2021"],
"module": "ES2020",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"allowJs": false,
"strict": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"outDir": "./dist",
"declaration": true
},
"include": ["./src"]
}