forked from forks/ffprobe-wasm-npm
Compare commits
6 Commits
v0.1.0
...
e7dbb8c0c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
e7dbb8c0c9
|
|||
|
cece48aaeb
|
|||
|
5214d5c530
|
|||
|
ff2bef3e9c
|
|||
|
|
193d2e7b9e | ||
|
|
e447a242c7 |
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
|||||||
[submodule "ffprobe-wasm-app"]
|
[submodule "ffprobe-wasm-app"]
|
||||||
path = ffprobe-wasm-app
|
path = ffprobe-wasm-app
|
||||||
url = https://github.com/alfg/ffprobe-wasm
|
url = https://git.cislabs.de/cis-oss/ffprobe-wasm
|
||||||
|
|||||||
1
.prettierrc.yml
Normal file
1
.prettierrc.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
trailingComma: all
|
||||||
24
README.md
24
README.md
@@ -1,11 +1,10 @@
|
|||||||
ffprobe-wasm
|
# ffprobe-wasm
|
||||||
==========
|
|
||||||
|
|
||||||
Gather information from multimedia streams. Works on the browser and Node.js.
|
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.
|
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.
|
_For limitations and recommendations, see [Notes section](#notes)._
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -18,25 +17,32 @@ npm install ffprobe-wasm
|
|||||||
Node.js
|
Node.js
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { FFprobeWorker } from 'ffprobe-wasm/node.mjs';
|
import { FFprobeWorker } from "ffprobe-wasm";
|
||||||
|
|
||||||
const worker = new FFprobeWorker();
|
const worker = new FFprobeWorker();
|
||||||
|
|
||||||
const fileInfo = await worker.getFileInfo('file.mp4');
|
const fileInfo = await worker.getFileInfo("file.mp4");
|
||||||
console.log(fileInfo);
|
console.log(fileInfo);
|
||||||
```
|
```
|
||||||
|
|
||||||
Browser
|
Browser
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { FFprobeWorker } from 'ffprobe-wasm/browser.mjs';
|
import { FFprobeWorker } from "ffprobe-wasm";
|
||||||
|
|
||||||
const worker = new FFprobeWorker();
|
const worker = new FFprobeWorker();
|
||||||
|
|
||||||
// input is the reference to a <input type="file" /> element
|
// input is the reference to an <input type="file" /> element
|
||||||
input.addEventListener('change', (event) => {
|
input.addEventListener("change", (event) => {
|
||||||
const file = event.target.files[0]
|
const file = event.target.files[0];
|
||||||
const fileInfo = await worker.getFileInfo(file);
|
const fileInfo = await worker.getFileInfo(file);
|
||||||
console.log(fileInfo);
|
console.log(fileInfo);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This project doesn't build or use FFprobe. Instead it uses FFmpeg's libavformat and libavcodec to output similar results. This means that not everything that FFprobe supports is bundled, so there are some containers and codecs that are not supported.
|
||||||
|
- In Node.js, it works on version >= 16.
|
||||||
|
- In browser, `SharedArrayBuffer` is being used. To enable this in your server, read [Security requirements](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements).
|
||||||
|
- In browser, everything is bundled in the `browser.mjs` script. When gzipped, this file is bigger than 1 MiB, so it's recommended to use `import()` to lazy load the asset. The good side of this is that you don't have to configure your bundler to include the worker or wasm files and you won't face [same-origin](https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker) issues with the worker.
|
||||||
|
|||||||
5
build.sh
5
build.sh
@@ -9,14 +9,17 @@ rm -rf ffprobe-wasm-app/dist
|
|||||||
|
|
||||||
# Build wasm
|
# Build wasm
|
||||||
cd ffprobe-wasm-app
|
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
|
docker-compose run ffprobe-wasm make
|
||||||
cd ..
|
cd ..
|
||||||
cp -R ffprobe-wasm-app/dist dist
|
cp -R ffprobe-wasm-app/dist dist
|
||||||
|
node scripts/replace.js
|
||||||
cp src/*.d.* dist
|
cp src/*.d.* dist
|
||||||
|
|
||||||
# Build browser/node workers
|
# Build browser/node workers
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
|
# Remove unnecessary files
|
||||||
|
rm dist/browser-vite.* dist/ffprobe-wasm.d.mts dist/ffprobe-wasm.mjs dist/worker-browser.*
|
||||||
|
|
||||||
# Copy files for npm publish
|
# Copy files for npm publish
|
||||||
cp package.json LICENSE README.md dist
|
cp package.json LICENSE README.md dist
|
||||||
|
|||||||
Submodule ffprobe-wasm-app updated: c0f96a1755...087e488ae9
1657
package-lock.json
generated
1657
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ffprobe-wasm",
|
"name": "ffprobe-wasm",
|
||||||
"version": "0.1.0",
|
"version": "0.3.0",
|
||||||
"description": "ffprobe-like for browser and node, powered by WebAssembly",
|
"description": "ffprobe-like for browser and node, powered by WebAssembly",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -17,11 +17,22 @@
|
|||||||
],
|
],
|
||||||
"author": "Tomás Fox <tomas.c.fox@gmail.com>",
|
"author": "Tomás Fox <tomas.c.fox@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"exports": {
|
||||||
|
"node": "./node.mjs",
|
||||||
|
"types": "./ffprobe-worker.d.mts",
|
||||||
|
"default": "./browser.mjs"
|
||||||
|
},
|
||||||
|
"main": "./node.mjs",
|
||||||
|
"types": "./ffprobe-worker.d.mts",
|
||||||
|
"browser": "./browser.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc"
|
"build": "npm run tsc && npm run vite",
|
||||||
|
"tsc": "tsc",
|
||||||
|
"vite": "vite build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^17.0.21",
|
"@types/node": "^25.2.3",
|
||||||
"typescript": "^4.5.5"
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
scripts/replace.js
Normal file
27
scripts/replace.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const { readFile, writeFile } = require("fs/promises");
|
||||||
|
const { dirname, resolve } = require("path");
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const root = dirname(__dirname);
|
||||||
|
|
||||||
|
const wasmJsPath = resolve(root, "dist/ffprobe-wasm.mjs");
|
||||||
|
|
||||||
|
let content = await readFile(wasmJsPath, { encoding: "utf8" });
|
||||||
|
|
||||||
|
content = `\
|
||||||
|
import initWasmInstance from "./ffprobe-wasm.wasm";
|
||||||
|
const initWasm = (info) =>
|
||||||
|
initWasmInstance(info).then((exports) => ({ instance: { exports } }));
|
||||||
|
${content}`;
|
||||||
|
|
||||||
|
content = content.replace(`import.meta.url`, `''`);
|
||||||
|
|
||||||
|
content = content.replace(
|
||||||
|
`instantiateAsync().catch(readyPromiseReject)`,
|
||||||
|
`initWasm(info).then(receiveInstantiatedSource, readyPromiseReject)`
|
||||||
|
);
|
||||||
|
|
||||||
|
await writeFile(wasmJsPath, content, { encoding: "utf8" });
|
||||||
|
}
|
||||||
88
src/browser-vite.mts
Normal file
88
src/browser-vite.mts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import type { FFprobeWorker as AbstractFFprobeWorker } from "./ffprobe-worker.mjs";
|
||||||
|
import type {
|
||||||
|
Chapter,
|
||||||
|
Disposition,
|
||||||
|
FileInfo,
|
||||||
|
Format,
|
||||||
|
Frame,
|
||||||
|
FramesInfo,
|
||||||
|
Rational,
|
||||||
|
Stream,
|
||||||
|
} from "./types.mjs";
|
||||||
|
import BrowserWorker from "./worker-browser.mjs?worker&inline";
|
||||||
|
import type {
|
||||||
|
IncomingMessage,
|
||||||
|
IncomingData,
|
||||||
|
OutgoingMessage,
|
||||||
|
} from "./worker.mjs";
|
||||||
|
|
||||||
|
export class FFprobeWorker implements AbstractFFprobeWorker {
|
||||||
|
readonly #worker: Worker;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.#worker = new BrowserWorker();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFileInfo(file: File): Promise<FileInfo> {
|
||||||
|
this.#validateFile(file);
|
||||||
|
const fileInfo: FileInfo = await this.#postMessage({
|
||||||
|
type: "getFileInfo",
|
||||||
|
payload: [file.name, { files: [file] }],
|
||||||
|
});
|
||||||
|
fileInfo.format.filename = file.name;
|
||||||
|
fileInfo.format.size = file.size.toString();
|
||||||
|
return fileInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFrames(file: File, offset: number): Promise<FramesInfo> {
|
||||||
|
this.#validateFile(file);
|
||||||
|
return this.#postMessage({
|
||||||
|
type: "getFrames",
|
||||||
|
payload: [file.name, { files: [file] }, offset],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate(): void {
|
||||||
|
this.#worker.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#validateFile(file: File | string): asserts file is File {
|
||||||
|
if (typeof file === "string") {
|
||||||
|
throw new Error(
|
||||||
|
"String only supported in Node.js, you must provide a File",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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,
|
||||||
|
Disposition,
|
||||||
|
FileInfo,
|
||||||
|
Format,
|
||||||
|
Frame,
|
||||||
|
FramesInfo,
|
||||||
|
Rational,
|
||||||
|
Stream,
|
||||||
|
};
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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 };
|
|
||||||
1
src/declarations/vite.d.ts
vendored
Normal file
1
src/declarations/vite.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { FileInfo, FramesInfo } from "./types.mjs";
|
||||||
|
|
||||||
export interface FFprobe {
|
export interface FFprobe {
|
||||||
get_file_info(path: string): Raw<FileInfo>;
|
get_file_info(path: string): Raw<FileInfo>;
|
||||||
get_frames(path: string, offset: number): Raw<FramesInfo>;
|
get_frames(path: string, offset: number): Raw<FramesInfo>;
|
||||||
@@ -69,77 +71,25 @@ export interface AnalyzePathReturn {
|
|||||||
parentObject: any | null;
|
parentObject: any | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileInfo {
|
export interface Vector<T> {
|
||||||
bit_rate: number
|
count: { value: 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;
|
ptr: number;
|
||||||
ptrType: any;
|
ptrType: any;
|
||||||
get(index: number): T;
|
get(index: number): T;
|
||||||
size(): number;
|
size(): number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DictionaryEntry {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type Raw<T> = {
|
export type Raw<T> = {
|
||||||
[K in keyof T]: T[K] extends Array<infer U> ? Collection<Raw<U>> : T[K]
|
[K in keyof T]: T[K] extends Array<infer U>
|
||||||
}
|
? Vector<Raw<U>>
|
||||||
|
: T[K] extends Record<string, string>
|
||||||
export interface Chapter {
|
? Vector<DictionaryEntry>
|
||||||
end: number
|
: T[K] extends string | number | boolean | undefined | null
|
||||||
id: number
|
? T[K]
|
||||||
start: number
|
: Raw<T[K]>;
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FFprobe } from "./ffprobe-wasm-shared";
|
import { FFprobe } from "./ffprobe-wasm-shared.mjs";
|
||||||
|
|
||||||
export * from "./ffprobe-wasm-shared";
|
export * from "./ffprobe-wasm-shared.mjs";
|
||||||
|
|
||||||
export default function loadFFprobe(ffprobe?: FFprobeInit): Promise<FFprobe>;
|
export default function loadFFprobe(ffprobe?: FFprobeInit): Promise<FFprobe>;
|
||||||
|
|
||||||
|
|||||||
4
src/ffprobe-wasm.d.ts
vendored
4
src/ffprobe-wasm.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
import { FFprobe } from "./ffprobe-wasm-shared";
|
import { FFprobe } from "./ffprobe-wasm-shared.mjs";
|
||||||
|
|
||||||
export * from "./ffprobe-wasm-shared";
|
export * from "./ffprobe-wasm-shared.mjs";
|
||||||
|
|
||||||
declare const ffprobe: FFprobe;
|
declare const ffprobe: FFprobe;
|
||||||
|
|
||||||
|
|||||||
17
src/ffprobe-worker.d.mts
Normal file
17
src/ffprobe-worker.d.mts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { FileInfo, FramesInfo } from "./types.mjs";
|
||||||
|
|
||||||
|
export * from "./types.mjs";
|
||||||
|
|
||||||
|
export declare class FFprobeWorker {
|
||||||
|
/**
|
||||||
|
* This function tries to be equivalent to
|
||||||
|
* ```
|
||||||
|
* ffprobe -hide_banner -loglevel fatal -show_format -show_streams -show_chapters -show_private_data -print_format json
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
getFileInfo(file: File | string): Promise<FileInfo>;
|
||||||
|
|
||||||
|
getFrames(file: File | string, offset: number): Promise<FramesInfo>;
|
||||||
|
|
||||||
|
terminate(): void;
|
||||||
|
}
|
||||||
53
src/node.mts
53
src/node.mts
@@ -1,14 +1,25 @@
|
|||||||
|
import { stat } from "fs/promises";
|
||||||
import { basename, dirname } from "path";
|
import { basename, dirname } from "path";
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from "url";
|
||||||
import { MessageChannel, Worker } from "worker_threads";
|
import { MessageChannel, Worker } from "worker_threads";
|
||||||
import type { Chapter, ChapterTag, FileInfo, Frame, FramesInfo, Stream } from "./ffprobe-wasm.mjs";
|
import type { FFprobeWorker as AbstractFFprobeWorker } from "./ffprobe-worker.mjs";
|
||||||
|
import type {
|
||||||
|
Chapter,
|
||||||
|
Disposition,
|
||||||
|
FileInfo,
|
||||||
|
Format,
|
||||||
|
Frame,
|
||||||
|
FramesInfo,
|
||||||
|
Rational,
|
||||||
|
Stream,
|
||||||
|
} from "./types.mjs";
|
||||||
import type {
|
import type {
|
||||||
IncomingMessage,
|
IncomingMessage,
|
||||||
IncomingData,
|
IncomingData,
|
||||||
OutgoingMessage,
|
OutgoingMessage,
|
||||||
} from "./worker.mjs";
|
} from "./worker.mjs";
|
||||||
|
|
||||||
export class FFprobeWorker {
|
export class FFprobeWorker implements AbstractFFprobeWorker {
|
||||||
readonly #worker: Worker;
|
readonly #worker: Worker;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -17,22 +28,41 @@ export class FFprobeWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getFileInfo(filePath: string): Promise<FileInfo> {
|
async getFileInfo(filePath: string): Promise<FileInfo> {
|
||||||
return this.#postMessage({ type: "getFileInfo", payload: [basename(filePath), { root: dirname(filePath) }] });
|
this.#validateFile(filePath);
|
||||||
|
const fileInfo: FileInfo = await this.#postMessage({
|
||||||
|
type: "getFileInfo",
|
||||||
|
payload: [basename(filePath), { root: dirname(filePath) }],
|
||||||
|
});
|
||||||
|
fileInfo.format.filename = filePath;
|
||||||
|
fileInfo.format.size = (await stat(filePath)).size.toString();
|
||||||
|
return fileInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFrames(filePath: string, offset: number): Promise<FramesInfo> {
|
async getFrames(filePath: string, offset: number): Promise<FramesInfo> {
|
||||||
return this.#postMessage({ type: "getFrames", payload: [basename(filePath), { root: dirname(filePath) }, offset] });
|
this.#validateFile(filePath);
|
||||||
|
return this.#postMessage({
|
||||||
|
type: "getFrames",
|
||||||
|
payload: [basename(filePath), { root: dirname(filePath) }, offset],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
terminate(): void {
|
terminate(): void {
|
||||||
this.#worker.terminate();
|
this.#worker.terminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#validateFile(filePath: string | File): asserts filePath is string {
|
||||||
|
if (typeof filePath === "object") {
|
||||||
|
throw new Error(
|
||||||
|
"File object only supported in Browser, you must provide a string (path)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#postMessage(data: IncomingData): Promise<any> {
|
#postMessage(data: IncomingData): Promise<any> {
|
||||||
const channel = new MessageChannel();
|
const channel = new MessageChannel();
|
||||||
const message: IncomingMessage = {
|
const message: IncomingMessage = {
|
||||||
...data,
|
...data,
|
||||||
port: channel.port2
|
port: channel.port2,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.#worker.postMessage(message, [channel.port2]);
|
this.#worker.postMessage(message, [channel.port2]);
|
||||||
@@ -49,4 +79,13 @@ export class FFprobeWorker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { Chapter, ChapterTag, FileInfo, Frame, FramesInfo, Stream };
|
export type {
|
||||||
|
Chapter,
|
||||||
|
Disposition,
|
||||||
|
FileInfo,
|
||||||
|
Format,
|
||||||
|
Frame,
|
||||||
|
FramesInfo,
|
||||||
|
Rational,
|
||||||
|
Stream,
|
||||||
|
};
|
||||||
|
|||||||
131
src/types.d.mts
Normal file
131
src/types.d.mts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
export type Rational = `${number}/${number}`;
|
||||||
|
|
||||||
|
export interface FileInfo {
|
||||||
|
streams: Stream[];
|
||||||
|
chapters: Chapter[];
|
||||||
|
format: Format;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Format {
|
||||||
|
filename: string;
|
||||||
|
nb_streams: number;
|
||||||
|
nb_programs: number;
|
||||||
|
format_name: string;
|
||||||
|
format_long_name: string;
|
||||||
|
start_time: string;
|
||||||
|
duration: string;
|
||||||
|
size: string;
|
||||||
|
bit_rate: string;
|
||||||
|
probe_score: number;
|
||||||
|
tags: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Chapter {
|
||||||
|
/**
|
||||||
|
* Chapter end time in time_base units
|
||||||
|
*/
|
||||||
|
end: number;
|
||||||
|
/**
|
||||||
|
* unique ID to identify the chapter
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
tags: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* Chapter start time in time_base units
|
||||||
|
*/
|
||||||
|
start: number;
|
||||||
|
/**
|
||||||
|
* Time base in which the start/end timestamps are specified
|
||||||
|
* @example "1/1000"
|
||||||
|
*/
|
||||||
|
time_base: Rational;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Stream {
|
||||||
|
index: number;
|
||||||
|
codec_name: string;
|
||||||
|
codec_long_name: string;
|
||||||
|
profile: string;
|
||||||
|
codec_type: string;
|
||||||
|
codec_tag_string: string;
|
||||||
|
codec_tag: string;
|
||||||
|
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
codec_width: number;
|
||||||
|
codec_height: number;
|
||||||
|
closed_captions: number;
|
||||||
|
has_b_frames: number;
|
||||||
|
pix_fmt: string;
|
||||||
|
level: number;
|
||||||
|
color_range: string;
|
||||||
|
color_primaries: string;
|
||||||
|
chroma_location: string;
|
||||||
|
refs: number;
|
||||||
|
is_avc: string;
|
||||||
|
nal_length_size: string;
|
||||||
|
|
||||||
|
sample_fmt: string;
|
||||||
|
sample_rate: string;
|
||||||
|
channels: number;
|
||||||
|
channel_layout: string;
|
||||||
|
bits_per_sample: number;
|
||||||
|
|
||||||
|
r_frame_rate: string;
|
||||||
|
avg_frame_rate: string;
|
||||||
|
/**
|
||||||
|
* This is the fundamental unit of time (in seconds) in terms
|
||||||
|
* of which frame timestamps are represented.
|
||||||
|
*/
|
||||||
|
time_base: Rational;
|
||||||
|
start_pts: number;
|
||||||
|
start_time: string;
|
||||||
|
duration_ts: number;
|
||||||
|
/**
|
||||||
|
* Duration of the stream, in stream time base.
|
||||||
|
* If a source file does not specify a duration, but does specify
|
||||||
|
* a bitrate, this value will be estimated from bitrate and file size.
|
||||||
|
*/
|
||||||
|
duration: string;
|
||||||
|
/**
|
||||||
|
* Total stream bitrate in bit/s, 0 if not available.
|
||||||
|
*/
|
||||||
|
bit_rate: string;
|
||||||
|
bits_per_raw_sample: string;
|
||||||
|
nb_frames: string;
|
||||||
|
disposition: Disposition;
|
||||||
|
tags: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Disposition {
|
||||||
|
default: 0 | 1;
|
||||||
|
dub: 0 | 1;
|
||||||
|
original: 0 | 1;
|
||||||
|
comment: 0 | 1;
|
||||||
|
lyrics: 0 | 1;
|
||||||
|
karaoke: 0 | 1;
|
||||||
|
forced: 0 | 1;
|
||||||
|
hearing_impaired: 0 | 1;
|
||||||
|
visual_impaired: 0 | 1;
|
||||||
|
clean_effects: 0 | 1;
|
||||||
|
attached_pic: 0 | 1;
|
||||||
|
timed_thumbnails: 0 | 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
import loadFFprobe from "./ffprobe-wasm.mjs";
|
import loadFFprobe from "./ffprobe-wasm.mjs";
|
||||||
import { createListener, IncomingMessage } from "./worker.mjs";
|
import { createListener, IncomingMessage } from "./worker.mjs";
|
||||||
|
|
||||||
const listener = createListener(
|
const listener = createListener(loadFFprobe(), "WORKERFS");
|
||||||
loadFFprobe({
|
|
||||||
locateFile: (path) => `${location.origin}/node_modules/ffprobe-wasm/${path}`
|
|
||||||
}),
|
|
||||||
"WORKERFS",
|
|
||||||
);
|
|
||||||
|
|
||||||
self.onmessage = (event: MessageEvent<IncomingMessage>) => listener(event.data);
|
self.onmessage = (event: MessageEvent<IncomingMessage>) => listener(event.data);
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import type { MessagePort as NodeMessagePort } from "worker_threads";
|
import type { MessagePort as NodeMessagePort } from "worker_threads";
|
||||||
import type { Chapter, ChapterTag, FFprobe, FileInfo, FramesInfo, FSFilesystems, FSMountOptions, Stream } from "./ffprobe-wasm-shared";
|
import type {
|
||||||
|
DictionaryEntry,
|
||||||
|
FFprobe,
|
||||||
|
FSFilesystems,
|
||||||
|
FSMountOptions,
|
||||||
|
Raw,
|
||||||
|
Vector,
|
||||||
|
} from "./ffprobe-wasm-shared.mjs";
|
||||||
|
import { FileInfo, FramesInfo, Stream } from "./types.mjs";
|
||||||
|
|
||||||
export type IncomingMessage = { port: MessagePort | NodeMessagePort } & IncomingData;
|
export type IncomingMessage = {
|
||||||
|
port: MessagePort | NodeMessagePort;
|
||||||
|
} & IncomingData;
|
||||||
|
|
||||||
export type IncomingData =
|
export type IncomingData =
|
||||||
| {
|
| {
|
||||||
@@ -63,7 +73,33 @@ export function createListener(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFileInfo(fileName: string, mountOptions: FSMountOptions): Promise<FileInfo> {
|
function vectorToArray<T>(vector: Vector<T>): T[] {
|
||||||
|
const array: T[] = [];
|
||||||
|
for (let i = 0; i < vector.size(); i++) {
|
||||||
|
array.push(vector.get(i));
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dictionaryVectorToRecord(
|
||||||
|
vector: Vector<DictionaryEntry>,
|
||||||
|
): Record<string, string> {
|
||||||
|
return Object.fromEntries(
|
||||||
|
vectorToArray(vector).map(({ key, value }) => [key, value]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeStreams(streams: Vector<Raw<Stream>>) {
|
||||||
|
return vectorToArray(streams).map((stream) => ({
|
||||||
|
...stream,
|
||||||
|
tags: dictionaryVectorToRecord(stream.tags),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileInfo(
|
||||||
|
fileName: string,
|
||||||
|
mountOptions: FSMountOptions,
|
||||||
|
): Promise<FileInfo> {
|
||||||
const { FS, get_file_info } = await ffprobePromise;
|
const { FS, get_file_info } = await ffprobePromise;
|
||||||
try {
|
try {
|
||||||
if (!FS.analyzePath("/work").exists) {
|
if (!FS.analyzePath("/work").exists) {
|
||||||
@@ -71,32 +107,18 @@ export function createListener(
|
|||||||
}
|
}
|
||||||
FS.mount(FS.filesystems[fsType], mountOptions, "/work");
|
FS.mount(FS.filesystems[fsType], mountOptions, "/work");
|
||||||
|
|
||||||
// Call the wasm module.
|
|
||||||
const rawInfo = get_file_info(`/work/${fileName}`);
|
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 {
|
return {
|
||||||
...rawInfo,
|
streams: serializeStreams(rawInfo.streams),
|
||||||
streams,
|
chapters: vectorToArray(rawInfo.chapters).map((chapter) => ({
|
||||||
chapters,
|
...chapter,
|
||||||
|
tags: dictionaryVectorToRecord(chapter.tags),
|
||||||
|
})),
|
||||||
|
format: {
|
||||||
|
...rawInfo.format,
|
||||||
|
tags: dictionaryVectorToRecord(rawInfo.format.tags),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
// Cleanup mount.
|
// Cleanup mount.
|
||||||
@@ -104,7 +126,11 @@ export function createListener(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFrames(fileName: string, mountOptions: FSMountOptions, offset: number): Promise<FramesInfo> {
|
async function getFrames(
|
||||||
|
fileName: string,
|
||||||
|
mountOptions: FSMountOptions,
|
||||||
|
offset: number,
|
||||||
|
): Promise<FramesInfo> {
|
||||||
const { FS, get_frames } = await ffprobePromise;
|
const { FS, get_frames } = await ffprobePromise;
|
||||||
try {
|
try {
|
||||||
if (!FS.analyzePath("/work").exists) {
|
if (!FS.analyzePath("/work").exists) {
|
||||||
@@ -112,18 +138,11 @@ export function createListener(
|
|||||||
}
|
}
|
||||||
FS.mount(FS.filesystems.WORKERFS, mountOptions, "/work");
|
FS.mount(FS.filesystems.WORKERFS, mountOptions, "/work");
|
||||||
|
|
||||||
// Call the wasm module.
|
|
||||||
const framesInfo = get_frames(`/work/${fileName}`, offset);
|
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 {
|
return {
|
||||||
...framesInfo,
|
...framesInfo,
|
||||||
frames,
|
frames: vectorToArray(framesInfo.frames),
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
// Cleanup mount.
|
// Cleanup mount.
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"declaration": true
|
"declaration": true,
|
||||||
|
"sourceMap": true
|
||||||
},
|
},
|
||||||
"include": ["./src"]
|
"include": ["./src"]
|
||||||
}
|
}
|
||||||
|
|||||||
31
vite.config.ts
Normal file
31
vite.config.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { resolve } from "path";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
outDir: resolve(__dirname, "dist"),
|
||||||
|
lib: {
|
||||||
|
entry: resolve(__dirname, "dist/browser-vite.mjs"),
|
||||||
|
formats: ["es"],
|
||||||
|
fileName: () => "browser.mjs",
|
||||||
|
},
|
||||||
|
emptyOutDir: false,
|
||||||
|
minify: false,
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
worker: {
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: "append-source-url",
|
||||||
|
generateBundle(options, bundle) {
|
||||||
|
Object.entries(bundle).forEach(([file, output]) => {
|
||||||
|
if (output.type === "chunk") {
|
||||||
|
output.code += `\n//# sourceURL=${file}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user