forked from forks/ffprobe-wasm-npm
Compare commits
17 Commits
v0.1.0
...
4bc9acdada
| Author | SHA1 | Date | |
|---|---|---|---|
|
4bc9acdada
|
|||
|
f97fb957dc
|
|||
|
d9013772ef
|
|||
|
fad46dbae1
|
|||
|
98151ecaef
|
|||
|
efa4e6f0a2
|
|||
|
24a55c018f
|
|||
|
bc9e1670b2
|
|||
|
c0e13cdaa1
|
|||
|
3d1951c50a
|
|||
|
90864d1b05
|
|||
|
e7dbb8c0c9
|
|||
|
cece48aaeb
|
|||
|
5214d5c530
|
|||
|
ff2bef3e9c
|
|||
|
|
193d2e7b9e | ||
|
|
e447a242c7 |
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -1,3 +1,3 @@
|
||||
[submodule "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.
|
||||
|
||||
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
|
||||
|
||||
@@ -18,25 +17,32 @@ npm install ffprobe-wasm
|
||||
Node.js
|
||||
|
||||
```ts
|
||||
import { FFprobeWorker } from 'ffprobe-wasm/node.mjs';
|
||||
import { FFprobeWorker } from "ffprobe-wasm";
|
||||
|
||||
const worker = new FFprobeWorker();
|
||||
|
||||
const fileInfo = await worker.getFileInfo('file.mp4');
|
||||
const fileInfo = await worker.getFileInfo("file.mp4");
|
||||
console.log(fileInfo);
|
||||
```
|
||||
|
||||
Browser
|
||||
|
||||
```ts
|
||||
import { FFprobeWorker } from 'ffprobe-wasm/browser.mjs';
|
||||
import { FFprobeWorker } from "ffprobe-wasm";
|
||||
|
||||
const worker = new FFprobeWorker();
|
||||
|
||||
// input is the reference to a <input type="file" /> element
|
||||
input.addEventListener('change', (event) => {
|
||||
const file = event.target.files[0]
|
||||
// input is the reference to an <input type="file" /> element
|
||||
input.addEventListener("change", (event) => {
|
||||
const file = event.target.files[0];
|
||||
const fileInfo = await worker.getFileInfo(file);
|
||||
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.
|
||||
|
||||
4
build.sh
4
build.sh
@@ -9,7 +9,6 @@ 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
|
||||
@@ -18,5 +17,8 @@ cp src/*.d.* dist
|
||||
# Build browser/node workers
|
||||
npm run build
|
||||
|
||||
# Remove unnecessary files
|
||||
rm dist/browser-vite.* dist/worker-browser.*
|
||||
|
||||
# Copy files for npm publish
|
||||
cp package.json LICENSE README.md dist
|
||||
|
||||
Submodule ffprobe-wasm-app updated: c0f96a1755...62ae9d08f0
2093
package-lock.json
generated
2093
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -1,13 +1,10 @@
|
||||
{
|
||||
"name": "ffprobe-wasm",
|
||||
"version": "0.1.0",
|
||||
"name": "@cis-oss/avprobe-wasm",
|
||||
"version": "0.3.2",
|
||||
"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"
|
||||
"url": "https://git.cislabs.de/cis-oss/ffprobe-wasm-npm"
|
||||
},
|
||||
"keywords": [
|
||||
"ffprobe",
|
||||
@@ -15,13 +12,30 @@
|
||||
"ffmpeg",
|
||||
"video"
|
||||
],
|
||||
"author": "Tomás Fox <tomas.c.fox@gmail.com>",
|
||||
"author": {
|
||||
"name": "Alix von Schirp",
|
||||
"url": "https://b00tload.space",
|
||||
"email": "hi@b00tload.space"
|
||||
},
|
||||
"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": {
|
||||
"build": "tsc"
|
||||
"build": "npm run tsc && npm run vite",
|
||||
"tsc": "tsc",
|
||||
"vite": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^17.0.21",
|
||||
"typescript": "^4.5.5"
|
||||
"@types/node": "^25.2.3",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-top-level-await": "^1.6.0",
|
||||
"vite-plugin-wasm": "^3.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
get_file_info(path: string): Raw<FileInfo>;
|
||||
get_frames(path: string, offset: number): Raw<FramesInfo>;
|
||||
@@ -69,77 +71,25 @@ export interface AnalyzePathReturn {
|
||||
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 }
|
||||
export interface Vector<T> {
|
||||
count: { value: number };
|
||||
ptr: number;
|
||||
ptrType: any;
|
||||
get(index: number): T;
|
||||
size(): number;
|
||||
}
|
||||
|
||||
export interface DictionaryEntry {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
[K in keyof T]: T[K] extends Array<infer U>
|
||||
? Vector<Raw<U>>
|
||||
: T[K] extends Record<string, string>
|
||||
? Vector<DictionaryEntry>
|
||||
: T[K] extends string | number | boolean | undefined | null
|
||||
? T[K]
|
||||
: Raw<T[K]>;
|
||||
};
|
||||
@@ -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>;
|
||||
|
||||
|
||||
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;
|
||||
|
||||
|
||||
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 { fileURLToPath } from 'url';
|
||||
import { fileURLToPath } from "url";
|
||||
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 {
|
||||
IncomingMessage,
|
||||
IncomingData,
|
||||
OutgoingMessage,
|
||||
} from "./worker.mjs";
|
||||
|
||||
export class FFprobeWorker {
|
||||
export class FFprobeWorker implements AbstractFFprobeWorker {
|
||||
readonly #worker: Worker;
|
||||
|
||||
constructor() {
|
||||
@@ -17,22 +28,41 @@ export class FFprobeWorker {
|
||||
}
|
||||
|
||||
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> {
|
||||
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 {
|
||||
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> {
|
||||
const channel = new MessageChannel();
|
||||
const message: IncomingMessage = {
|
||||
...data,
|
||||
port: channel.port2
|
||||
port: 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 { createListener, IncomingMessage } from "./worker.mjs";
|
||||
|
||||
const listener = createListener(
|
||||
loadFFprobe({
|
||||
locateFile: (path) => `${location.origin}/node_modules/ffprobe-wasm/${path}`
|
||||
}),
|
||||
"WORKERFS",
|
||||
);
|
||||
const listener = createListener(loadFFprobe(), "WORKERFS");
|
||||
|
||||
self.onmessage = (event: MessageEvent<IncomingMessage>) => listener(event.data);
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
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 =
|
||||
| {
|
||||
@@ -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: (stream && stream.tags) ? dictionaryVectorToRecord(stream.tags) : {},
|
||||
}));
|
||||
}
|
||||
|
||||
async function getFileInfo(
|
||||
fileName: string,
|
||||
mountOptions: FSMountOptions,
|
||||
): Promise<FileInfo> {
|
||||
const { FS, get_file_info } = await ffprobePromise;
|
||||
try {
|
||||
if (!FS.analyzePath("/work").exists) {
|
||||
@@ -71,32 +107,21 @@ export function createListener(
|
||||
}
|
||||
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 });
|
||||
}
|
||||
// Safety check on rawInfo itself
|
||||
if (!rawInfo) throw new Error("FFprobe failed to return file info");
|
||||
|
||||
return {
|
||||
...rawInfo,
|
||||
streams,
|
||||
chapters,
|
||||
streams: serializeStreams(rawInfo.streams),
|
||||
chapters: rawInfo.chapters ? vectorToArray(rawInfo.chapters).map((chapter) => ({
|
||||
...chapter,
|
||||
tags: chapter.tags ? dictionaryVectorToRecord(chapter.tags) : {},
|
||||
})) : [],
|
||||
format: {
|
||||
...rawInfo.format,
|
||||
tags: (rawInfo.format && rawInfo.format.tags) ? dictionaryVectorToRecord(rawInfo.format.tags) : {},
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
// Cleanup mount.
|
||||
@@ -104,7 +129,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;
|
||||
try {
|
||||
if (!FS.analyzePath("/work").exists) {
|
||||
@@ -112,18 +141,11 @@ export function createListener(
|
||||
}
|
||||
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,
|
||||
frames: vectorToArray(framesInfo.frames),
|
||||
};
|
||||
} finally {
|
||||
// Cleanup mount.
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"outDir": "./dist",
|
||||
"declaration": true
|
||||
"declaration": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"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";
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
import wasm from "vite-plugin-wasm";
|
||||
|
||||
// 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,
|
||||
},
|
||||
plugins: [
|
||||
|
||||
wasm(),
|
||||
topLevelAwait()
|
||||
],
|
||||
worker: {
|
||||
plugins: () => [
|
||||
wasm(),
|
||||
topLevelAwait(),
|
||||
],
|
||||
format: "es"
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user