forked from forks/ffprobe-wasm-npm
Make file info output more similar to ffprobe
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import type { FFprobeWorker as AbstractFFprobeWorker } from "./ffprobe-worker.mjs";
|
||||
import type {
|
||||
Chapter,
|
||||
ChapterTag,
|
||||
Disposition,
|
||||
FileInfo,
|
||||
Format,
|
||||
Frame,
|
||||
FramesInfo,
|
||||
Rational,
|
||||
Stream,
|
||||
} from "./ffprobe-wasm.mjs";
|
||||
import type { FFprobeWorker as AbstractFFprobeWorker } from "./ffprobe-worker.js";
|
||||
} from "./types.mjs";
|
||||
import BrowserWorker from "./worker-browser.mjs?worker&inline";
|
||||
import type {
|
||||
IncomingMessage,
|
||||
@@ -23,10 +25,13 @@ export class FFprobeWorker implements AbstractFFprobeWorker {
|
||||
|
||||
async getFileInfo(file: File): Promise<FileInfo> {
|
||||
this.#validateFile(file);
|
||||
return this.#postMessage({
|
||||
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> {
|
||||
@@ -44,7 +49,7 @@ export class FFprobeWorker implements AbstractFFprobeWorker {
|
||||
#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"
|
||||
"String only supported in Node.js, you must provide a File",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -71,4 +76,13 @@ export class FFprobeWorker implements AbstractFFprobeWorker {
|
||||
}
|
||||
}
|
||||
|
||||
export type { Chapter, ChapterTag, FileInfo, Frame, FramesInfo, Stream };
|
||||
export type {
|
||||
Chapter,
|
||||
Disposition,
|
||||
FileInfo,
|
||||
Format,
|
||||
Frame,
|
||||
FramesInfo,
|
||||
Rational,
|
||||
Stream,
|
||||
};
|
||||
|
||||
@@ -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,22 +71,7 @@ export interface AnalyzePathReturn {
|
||||
parentObject: any | null;
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
bit_rate: number;
|
||||
chapters: Chapter[];
|
||||
/**
|
||||
* Duration in microseconds
|
||||
*/
|
||||
duration: number;
|
||||
flags: number;
|
||||
name: string;
|
||||
nb_chapters: number;
|
||||
nb_streams: number;
|
||||
streams: Stream[];
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface Collection<T> {
|
||||
export interface Vector<T> {
|
||||
count: { value: number };
|
||||
ptr: number;
|
||||
ptrType: any;
|
||||
@@ -92,57 +79,17 @@ export interface Collection<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 {
|
||||
export interface DictionaryEntry {
|
||||
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;
|
||||
}
|
||||
export type Raw<T> = {
|
||||
[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;
|
||||
}
|
||||
18
src/ffprobe-worker.d.ts
vendored
18
src/ffprobe-worker.d.ts
vendored
@@ -1,18 +0,0 @@
|
||||
import type {
|
||||
Chapter,
|
||||
ChapterTag,
|
||||
FileInfo,
|
||||
Frame,
|
||||
FramesInfo,
|
||||
Stream,
|
||||
} from "./ffprobe-wasm-shared";
|
||||
|
||||
export declare class FFprobeWorker {
|
||||
getFileInfo(file: File | string): Promise<FileInfo>;
|
||||
|
||||
getFrames(file: File | string, offset: number): Promise<FramesInfo>;
|
||||
|
||||
terminate(): void;
|
||||
}
|
||||
|
||||
export { Chapter, ChapterTag, FileInfo, Frame, FramesInfo, Stream };
|
||||
27
src/node.mts
27
src/node.mts
@@ -1,15 +1,18 @@
|
||||
import { stat } from "fs/promises";
|
||||
import { basename, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { MessageChannel, Worker } from "worker_threads";
|
||||
import type { FFprobeWorker as AbstractFFprobeWorker } from "./ffprobe-worker.mjs";
|
||||
import type {
|
||||
Chapter,
|
||||
ChapterTag,
|
||||
Disposition,
|
||||
FileInfo,
|
||||
Format,
|
||||
Frame,
|
||||
FramesInfo,
|
||||
Rational,
|
||||
Stream,
|
||||
} from "./ffprobe-wasm.js";
|
||||
import type { FFprobeWorker as AbstractFFprobeWorker } from "./ffprobe-worker.js";
|
||||
} from "./types.mjs";
|
||||
import type {
|
||||
IncomingMessage,
|
||||
IncomingData,
|
||||
@@ -26,10 +29,13 @@ export class FFprobeWorker implements AbstractFFprobeWorker {
|
||||
|
||||
async getFileInfo(filePath: string): Promise<FileInfo> {
|
||||
this.#validateFile(filePath);
|
||||
return this.#postMessage({
|
||||
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> {
|
||||
@@ -47,7 +53,7 @@ export class FFprobeWorker implements AbstractFFprobeWorker {
|
||||
#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)"
|
||||
"File object only supported in Browser, you must provide a string (path)",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -73,4 +79,13 @@ export class FFprobeWorker implements AbstractFFprobeWorker {
|
||||
}
|
||||
}
|
||||
|
||||
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,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: 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,18 @@ 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 });
|
||||
}
|
||||
|
||||
return {
|
||||
...rawInfo,
|
||||
streams,
|
||||
chapters,
|
||||
streams: serializeStreams(rawInfo.streams),
|
||||
chapters: vectorToArray(rawInfo.chapters).map((chapter) => ({
|
||||
...chapter,
|
||||
tags: dictionaryVectorToRecord(chapter.tags),
|
||||
})),
|
||||
format: {
|
||||
...rawInfo.format,
|
||||
tags: dictionaryVectorToRecord(rawInfo.format.tags),
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
// 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;
|
||||
try {
|
||||
if (!FS.analyzePath("/work").exists) {
|
||||
@@ -112,18 +138,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.
|
||||
|
||||
Reference in New Issue
Block a user