Make file info output more similar to ffprobe

This commit is contained in:
Tomás Fox
2022-03-17 17:48:33 -03:00
parent e447a242c7
commit 193d2e7b9e
14 changed files with 270 additions and 142 deletions

View File

@@ -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,
};

View File

@@ -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]>;
};

View File

@@ -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>;

View File

@@ -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
View 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;
}

View File

@@ -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 };

View File

@@ -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
View 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;
}

View File

@@ -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.