diff --git a/packages/ffmpeg/src/classes.ts b/packages/ffmpeg/src/classes.ts index bec0354fb35..0fd061c7f9e 100644 --- a/packages/ffmpeg/src/classes.ts +++ b/packages/ffmpeg/src/classes.ts @@ -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"; @@ -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: @@ -362,6 +367,120 @@ export class FFmpeg { ) as Promise; }; + /** + * 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 => { + return this.#send( + { + type: FFMessageType.OPEN, + data: { path, flags, mode }, + } + ) as Promise; + } + + /** + * 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 => { + return this.#send( + { + type: FFMessageType.CLOSE, + data: { fd }, + } + ) as Promise; + } + + /** + * 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 => { + return this.#send( + { + type: FFMessageType.READ, + data: { fd, offset, length, position }, + } + ) as Promise; + } + + /** + * 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 => { + const trans: Transferable[] = [buffer.buffer]; + + return this.#send( + { + type: FFMessageType.WRITE, + data: { fd, buffer, offset, length, position }, + }, + trans + ) as Promise; + } + /** * Read data from ffmpeg.wasm. * diff --git a/packages/ffmpeg/src/const.ts b/packages/ffmpeg/src/const.ts index 940651330e9..e82018ed871 100644 --- a/packages/ffmpeg/src/const.ts +++ b/packages/ffmpeg/src/const.ts @@ -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", diff --git a/packages/ffmpeg/src/errors.ts b/packages/ffmpeg/src/errors.ts index d7246c79ecd..10ec78b46ed 100644 --- a/packages/ffmpeg/src/errors.ts +++ b/packages/ffmpeg/src/errors.ts @@ -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"); diff --git a/packages/ffmpeg/src/types.ts b/packages/ffmpeg/src/types.ts index 6082a6d908d..cc3aede455b 100644 --- a/packages/ffmpeg/src/types.ts +++ b/packages/ffmpeg/src/types.ts @@ -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; @@ -110,6 +130,10 @@ export interface FFMessageUnmountData { export type FFMessageData = | FFMessageLoadConfig | FFMessageExecData + | FFMessageOpenData + | FFMessageCloseData + | FFMessageReadData + | FFMessageWriteData // eslint-disable-line | FFMessageWriteFileData | FFMessageReadFileData | FFMessageDeleteFileData @@ -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; @@ -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 diff --git a/packages/ffmpeg/src/worker.ts b/packages/ffmpeg/src/worker.ts index cf8b6c5aac2..77ffb5bbd2f 100644 --- a/packages/ffmpeg/src/worker.ts +++ b/packages/ffmpeg/src/worker.ts @@ -2,7 +2,7 @@ /// /// -import type { FFmpegCoreModule, FFmpegCoreModuleFactory } from "@ffmpeg/types"; +import type { FFmpegCoreModule, FFmpegCoreModuleFactory, FSStream } from "@ffmpeg/types"; import type { FFMessageEvent, FFMessageLoadConfig, @@ -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 { @@ -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); @@ -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; diff --git a/packages/types/types/index.d.ts b/packages/types/types/index.d.ts index 8795007c154..2f7d62a04c8 100644 --- a/packages/types/types/index.d.ts +++ b/packages/types/types/index.d.ts @@ -62,6 +62,10 @@ export interface WorkerFSMountConfig { files?: File[]; } +export interface FSStream { + fd: number; +} + /** * Functions to interact with Emscripten FS library. * @@ -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[];