Skip to content

Add support for FS.open, FS.close, FS.read, FS.write, #834

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions packages/ffmpeg/src/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
FFFSType,
FFFSMountOptions,
FFFSPath,
FileReadData,
} from "./types.js";
import { getMessageID } from "./utils.js";
import { ERROR_TERMINATED, ERROR_NOT_LOADED } from "./errors.js";
Expand Down Expand Up @@ -63,6 +64,10 @@ export class FFmpeg {
case FFMessageType.UNMOUNT:
case FFMessageType.EXEC:
case FFMessageType.FFPROBE:
case FFMessageType.OPEN:
case FFMessageType.CLOSE:
case FFMessageType.READ:
case FFMessageType.WRITE:
case FFMessageType.WRITE_FILE:
case FFMessageType.READ_FILE:
case FFMessageType.DELETE_FILE:
Expand Down Expand Up @@ -362,6 +367,120 @@ export class FFmpeg {
) as Promise<OK>;
};

/**
* Opens a file with the specified path, flags, and mode.
*
* @returns A file descriptor number.
* @category File System
*/
public open = (
/** The path to the file. */
path: string,
/**
* Mode for opening the file (e.g., 'r', 'w', 'a')
* @see [FS read and write flags](https://emscripten.org/docs/api_reference/Filesystem-API.html#fs-read-and-write-flags)
* */
flags: string,
/**
* Permissions for creating a new file.
* @defaultValue 0666
* */
mode?: number
): Promise<number> => {
return this.#send(
{
type: FFMessageType.OPEN,
data: { path, flags, mode },
}
) as Promise<number>;
}

/**
* Closes an open file descriptor.
*
* @returns Resolves when the file is successfully closed.
* @category File System
*/
public close = (
/** The file descriptor to close. */
fd: number
): Promise<OK> => {
return this.#send(
{
type: FFMessageType.CLOSE,
data: { fd },
}
) as Promise<OK>;
}

/**
* Reads data from an open file descriptor.
* @example
* ```ts
* const ffmpeg = new FFmpeg();
* await ffmpeg.load();
* const fd = await ffmpeg.open("../video.avi");
* const CHUNK_SIZE = 1024;
* const { data, done } = await ffmpeg.read(fd, 0, CHUNK_SIZE)
* await ffmpeg.close(fd);
* ```
* @category File System
*/
public read = (
/** The file descriptor to read from. */
fd: number,
/** The offset in the buffer to start writing data. */
offset: number,
/** The number of bytes to read. */
length: number,
/** The offset within the stream to read. By default this is the stream’s current offset. */
position?: number
): Promise<FileReadData> => {
return this.#send(
{
type: FFMessageType.READ,
data: { fd, offset, length, position },
}
) as Promise<FileReadData>;
}

/**
* Writes data to an open file descriptor.
*
* @example
* ```ts
* const ffmpeg = new FFmpeg();
* await ffmpeg.load();
* const data = new Uint8Array(32);
* const fd = await ffmpeg.open("../video.avi", "w+");
* await ffmpeg.write(fd, data, 0, data.length, 0);
* await ffmpeg.close(fd);
* ```
* @category File System
*/
public write = (
/** The file descriptor to write to. */
fd: number,
/** The buffer containing the data to write. */
buffer: Uint8Array,
/** The offset in the buffer to start writing from. */
offset: number,
/** The number of bytes to write. */
length: number,
/** The offset within the stream to write. By default this is the stream’s current offset. */
position?: number
): Promise<OK> => {
const trans: Transferable[] = [buffer.buffer];

return this.#send(
{
type: FFMessageType.WRITE,
data: { fd, buffer, offset, length, position },
},
trans
) as Promise<OK>;
}

/**
* Read data from ffmpeg.wasm.
*
Expand Down
4 changes: 4 additions & 0 deletions packages/ffmpeg/src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export enum FFMessageType {
LOAD = "LOAD",
EXEC = "EXEC",
FFPROBE = "FFPROBE",
OPEN = "OPEN",
CLOSE = "CLOSE",
READ = "READ",
WRITE = "WRITE",
WRITE_FILE = "WRITE_FILE",
READ_FILE = "READ_FILE",
DELETE_FILE = "DELETE_FILE",
Expand Down
2 changes: 2 additions & 0 deletions packages/ffmpeg/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export const ERROR_TERMINATED = new Error("called FFmpeg.terminate()");
export const ERROR_IMPORT_FAILURE = new Error(
"failed to import ffmpeg-core.js"
);

export const ERROR_FS_STREAM_NOT_FOUND = new Error("FS stream not found");
32 changes: 32 additions & 0 deletions packages/ffmpeg/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ export interface FFMessageExecData {
timeout?: number;
}

export interface FFMessageOpenData {
path: string;
flags: string;
mode?: number;
}

export interface FFMessageCloseData {
fd: number;
}

export interface FFMessageReadData {
fd: number;
buffer: Uint8Array;
offset: number;
length: number;
position?: number;
}

export type FFMessageWriteData = FFMessageReadData;

export interface FFMessageWriteFileData {
path: FFFSPath;
data: FileData;
Expand Down Expand Up @@ -110,6 +130,10 @@ export interface FFMessageUnmountData {
export type FFMessageData =
| FFMessageLoadConfig
| FFMessageExecData
| FFMessageOpenData
| FFMessageCloseData
| FFMessageReadData
| FFMessageWriteData // eslint-disable-line
| FFMessageWriteFileData
| FFMessageReadFileData
| FFMessageDeleteFileData
Expand Down Expand Up @@ -146,6 +170,7 @@ export interface ProgressEvent {
export type ExitCode = number;
export type ErrorMessage = string;
export type FileData = Uint8Array | string;
export type FD = number;
export type IsFirst = boolean;
export type OK = boolean;

Expand All @@ -154,9 +179,16 @@ export interface FSNode {
isDir: boolean;
}

export interface FileReadData {
data?: Uint8Array,
done: boolean
}

export type CallbackData =
| FileData
| ExitCode
| FD // eslint-disable-line
| FileReadData
| ErrorMessage
| LogEvent
| ProgressEvent
Expand Down
58 changes: 57 additions & 1 deletion packages/ffmpeg/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/// <reference lib="esnext" />
/// <reference lib="webworker" />

import type { FFmpegCoreModule, FFmpegCoreModuleFactory } from "@ffmpeg/types";
import type { FFmpegCoreModule, FFmpegCoreModuleFactory, FSStream } from "@ffmpeg/types";
import type {
FFMessageEvent,
FFMessageLoadConfig,
Expand All @@ -22,12 +22,19 @@ import type {
ExitCode,
FSNode,
FileData,
FD,
FFMessageOpenData,
FFMessageCloseData,
FFMessageReadData,
FFMessageWriteData,
FileReadData,
} from "./types";
import { CORE_URL, FFMessageType } from "./const.js";
import {
ERROR_UNKNOWN_MESSAGE_TYPE,
ERROR_NOT_LOADED,
ERROR_IMPORT_FAILURE,
ERROR_FS_STREAM_NOT_FOUND
} from "./errors.js";

declare global {
Expand Down Expand Up @@ -116,6 +123,42 @@ const writeFile = ({ path, data }: FFMessageWriteFileData): OK => {
const readFile = ({ path, encoding }: FFMessageReadFileData): FileData =>
ffmpeg.FS.readFile(path, { encoding });

const open = ({ path, flags, mode }: FFMessageOpenData): FD => {
return ffmpeg.FS.open(path, flags, mode).fd;
}

const close = ({ fd }: FFMessageCloseData): OK => {
const stream = ffmpeg.FS.getStream(fd);
if (stream) {
ffmpeg.FS.close(stream);
}
return true;
}

const getStream = (fd: number): FSStream => {
const stream = ffmpeg.FS.getStream(fd);
if (!stream) throw ERROR_FS_STREAM_NOT_FOUND;
return stream;
}

const read = ({ fd, offset, length, position }: FFMessageReadData): FileReadData => {
const stream = getStream(fd);
const data = new Uint8Array(length);
const current = ffmpeg.FS.read(stream, data, offset, length, position)
if (current == 0) {
return { done: true }
} else if (current < data.length) {
return { data: data.subarray(0, current), done: false }
}
return { data, done: false }
}

const write = ({ fd, buffer, offset, length, position }: FFMessageWriteData): OK => {
const stream = getStream(fd);
ffmpeg.FS.write(stream, buffer, offset, length, position);
return true;
}

// TODO: check if deletion works.
const deleteFile = ({ path }: FFMessageDeleteFileData): OK => {
ffmpeg.FS.unlink(path);
Expand Down Expand Up @@ -181,6 +224,19 @@ self.onmessage = async ({
case FFMessageType.FFPROBE:
data = ffprobe(_data as FFMessageExecData);
break;
case FFMessageType.OPEN:
data = open(_data as FFMessageOpenData);
break;
case FFMessageType.CLOSE:
data = close(_data as FFMessageCloseData);
break;
case FFMessageType.READ:
data = read(_data as FFMessageReadData);
if (data.data) trans.push(data.data.buffer)
break;
case FFMessageType.WRITE:
data = write(_data as FFMessageWriteData);
break;
case FFMessageType.WRITE_FILE:
data = writeFile(_data as FFMessageWriteFileData);
break;
Expand Down
9 changes: 9 additions & 0 deletions packages/types/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ export interface WorkerFSMountConfig {
files?: File[];
}

export interface FSStream {
fd: number;
}

/**
* Functions to interact with Emscripten FS library.
*
Expand All @@ -72,6 +76,11 @@ export interface FS {
mkdir: (path: string) => void;
rmdir: (path: string) => void;
rename: (oldPath: string, newPath: string) => void;
open: (path: string, flags: string, mode?: number) => FSStream;
getStream: (fd: number) => FSStream | undefined;
close: (stream: FSStream) => void;
read: (stream: FSStream, buffer: Uint8Array, offset: number, length: number, position?: number) => number;
write: (stream: FSStream, buffer: Uint8Array, offset: number, length: number, position?: number) => void;
writeFile: (path: string, data: Uint8Array | string) => void;
readFile: (path: string, opts: OptionReadFile) => Uint8Array | string;
readdir: (path: string) => string[];
Expand Down