Skip to content

Commit

Permalink
added thumbnail generator and meta function
Browse files Browse the repository at this point in the history
  • Loading branch information
k9p5 committed Jul 8, 2023
1 parent 84d9179 commit 8cb7279
Show file tree
Hide file tree
Showing 14 changed files with 378 additions and 5 deletions.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
<p align="center">
<img src="./public/icon.png" alt="Library Icon" width="164" height="164" />
</p>

[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/diffusion-studio/ffmpeg-js/graphs/commit-activity)
[![Website shields.io](https://img.shields.io/website-up-down-green-red/http/shields.io.svg)](https://ffmpeg-js-preview.vercel.app)
[![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/n3mpzfejAb)
[![GitHub license](https://badgen.net/github/license/Naereen/Strapdown.js)](https://github.com/diffusion-studio/ffmpeg-js/blob/main/LICENSE)
[![TypeScript](https://badgen.net/badge/icon/typescript?icon=typescript&label)](https://typescriptlang.org)

# 🎥 FFmpeg.js: A WebAssembly-powered FFmpeg Interface for Browsers

Welcome to FFmpeg.js, an innovative library that offers a WebAssembly-powered interface for utilizing FFmpeg in the browser. 🌐💡

### [👥Join our Discord](https://discord.gg/n3mpzfejAb)

## Demo

![GIF Converter Demo](./public/preview.gif)
Expand Down Expand Up @@ -58,6 +66,7 @@ server: {
### △Next.js

Here is an example `next.config.js` that supports the SharedArrayBuffer:

```
module.exports = {
async headers() {
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@diffusion-studio/ffmpeg-js",
"private": false,
"version": "0.1.0",
"version": "0.2.0",
"description": "FFmpeg.js - Use FFmpeg in the browser powered by WebAssembly",
"type": "module",
"files": [
Expand All @@ -28,6 +28,7 @@
"keywords": [
"ffmpeg",
"webassembly",
"emscripten",
"audio",
"browser",
"video",
Expand All @@ -42,9 +43,14 @@
"mp3",
"wav",
"flac",
"mkv",
"mov",
"ogg",
"hevc",
"h264",
"h265",
"quicktime",
"matroska",
"editing",
"cutting"
],
Expand Down
Binary file added public/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/samples/video.mkv
Binary file not shown.
Binary file removed public/samples/video_xl.mp4
Binary file not shown.
2 changes: 1 addition & 1 deletion src/ffmpeg-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ export class FFmpegBase {
* have been written to the memfs memory
*/
public clearMemory(): void {
for (const path of this._memory) {
for (const path of [...new Set(this._memory)]) {
this.deleteFile(path);
}
this._memory = [];
Expand Down
78 changes: 77 additions & 1 deletion src/ffmpeg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { IFFmpegConfiguration } from './interfaces';
import { FFmpegBase } from './ffmpeg-base';
import * as types from './types';
import configs from './ffmpeg-config';
import { noop } from './utils';
import { noop, parseMetadata } from './utils';

export class FFmpeg<
Config extends IFFmpegConfiguration<
Expand Down Expand Up @@ -220,6 +220,82 @@ export class FFmpeg<
return file;
}

/**
* Get the meta data of a the specified file.
* Returns information such as codecs, fps, bitrate etc.
*/
public async meta(source: string | Blob): Promise<types.Metadata> {
await this.writeFile('probe', source);
const meta: types.Metadata = {
streams: { audio: [], video: [] },
};
const callback = parseMetadata(meta);
ffmpeg.onMessage(callback);
await this.exec(['-i', 'probe']);
ffmpeg.removeOnMessage(callback);
this.clearMemory();
return meta;
}

/**
* Generate a series of thumbnails
* @param source Your input file
* @param count The number of thumbnails to generate
* @param start Lower time limit in seconds
* @param stop Upper time limit in seconds
* @example
* // type AsyncGenerator<Blob, void, void>
* const generator = ffmpeg.thumbnails('/samples/video.mp4');
*
* for await (const image of generator) {
* const img = document.createElement('img');
* img.src = URL.createObjectURL(image);
* document.body.appendChild(img);
* }
*/
public async *thumbnails(
source: string | Blob,
count: number = 5,
start: number = 0,
stop?: number
): AsyncGenerator<Blob, void, void> {
// make sure start and stop are defined
if (!stop) {
const { duration } = await this.meta(source);

// make sure the duration is defined
if (duration) stop = duration;
else {
console.warn(
'Could not extract duration from meta data please provide a stop argument. Falling back to 1sec otherwise.'
);
stop = 1;
}
}

// get the time increase for each iteration
const step = (stop - start) / count;

await this.writeFile('input', source);

for (let i = start; i < stop; i += step) {
await ffmpeg.exec([
'-ss',
i.toString(),
'-i',
'input',
'-frames:v',
'1',
'image.jpg',
]);
try {
const res = await ffmpeg.readFile('image.jpg');
yield new Blob([res], { type: 'image/jpeg' });
} catch (e) {}
}
this.clearMemory();
}

private parseOutputOptions(): string[] {
if (!this._output) {
throw new Error('Please define the output first');
Expand Down
83 changes: 83 additions & 0 deletions src/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,86 @@ export type WasmModuleURIs = {
*/
worker: string;
};

/**
* Defines the metadata of an audio stream
*/
export type AudioStream = {
/**
* String containing the id given
* by ffmpeg, e.g. 0:1
*/
id?: string;
/**
* String containing the audio codec
*/
codec?: string;
/**
* Number containing the audio sample rate
*/
sampleRate?: number;
};

/**
* Defines the metadata of a video stream
*/
export type VideoStream = {
/**
* String containing the id given
* by ffmpeg, e.g. 0:0
*/
id?: string;
/**
* String containing the video codec
*/
codec?: string;
/**
* Number containing the video width
*/
width?: number;
/**
* Number containing the video height
*/
height?: number;
/**
* Number containing the fps
*/
fps?: number;
};

/**
* Defines the metadata of a ffmpeg input log.
* These information will be extracted from
* the -i command.
*/
export type Metadata = {
/**
* Number containing the duration of the
* input in seconds
*/
duration?: number;
/**
* String containing the bitrate of the file.
* E.g 16 kb/s
*/
bitrate?: string;
/**
* Array of strings containing the applicable
* container formats. E.g. mov, mp4, m4a,
* 3gp, 3g2, mj2
*/
formats?: string[];
/**
* Separation in audio and video streams
*/
streams: {
/**
* Array of audio streams
*/
audio: AudioStream[];
/**
* Array of video streams
*/
video: VideoStream[];
};
};
1 change: 1 addition & 0 deletions src/types/gpl-extended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export type ExtensionGPLExtended =
| 'mp4'
| 'mpg'
| 'mpeg'
| 'mkv'
| 'mov'
| 'ts'
| 'm2t'
Expand Down
1 change: 1 addition & 0 deletions src/types/lgpl-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export type ExtensionBase =
| 'mpg'
| 'mpeg'
| 'mov'
| 'mkv'
| 'ts'
| 'm2t'
| 'm2ts'
Expand Down
106 changes: 106 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as types from './types';

/**
* Get uint 8 array from a blob or url
*/
Expand Down Expand Up @@ -51,3 +53,107 @@ export const parseProgress = (msg: string): number => {

return parseInt(match ?? '0');
};

/**
* Parse a ffmpeg message and extract the meta
* data of the input
* @param data reference to the object that should
* recieve the data
* @returns Callback function that can be passed into
* the onMessage function
*/
export const parseMetadata = (data: types.Metadata) => (msg: string) => {
// this string contains the format of the input
if (msg.match(/Input #/)) {
Object.assign(data, {
formats: msg
.replace(/(Input #|from 'probe')/gm, '')
.split(',')
.map((f) => f.trim())
.filter((f) => f.length > 1),
});
}

// this string contains the duration of the input
if (msg.match(/Duration:/)) {
const splits = msg.split(',');
for (const split of splits) {
if (split.match(/Duration:/)) {
const duration = split.replace(/Duration:/, '').trim();
Object.assign(data, {
duration: Date.parse(`01 Jan 1970 ${duration} GMT`) / 1000,
});
}
if (split.match(/bitrate:/)) {
const bitrate = split.replace(/bitrate:/, '').trim();
Object.assign(data, { bitrate });
}
}
}

// there can be one or more streams
if (msg.match(/Stream #/)) {
const splits = msg.split(',');

// id is the same for all streams
const base = {
id: splits
?.at(0)
?.match(/[0-9]{1,2}:[0-9]{1,2}/)
?.at(0),
};

// match video streams
if (msg.match(/Video/)) {
const stream: types.VideoStream = base;
for (const split of splits) {
// match codec
if (split.match(/Video:/)) {
Object.assign(stream, {
codec: split
.match(/Video:\W*[a-z0-9_-]*\W/i)
?.at(0)
?.replace(/Video:/, '')
?.trim(),
});
}
// match size
if (split.match(/[0-9]*x[0-9]*/)) {
Object.assign(stream, { width: parseFloat(split.split('x')[0]) });
Object.assign(stream, { height: parseFloat(split.split('x')[1]) });
}
// match fps
if (split.match(/fps/)) {
Object.assign(stream, {
fps: parseFloat(split.replace('fps', '').trim()),
});
}
}
data.streams.video.push(stream);
}

// match audio streams
if (msg.match(/Audio/)) {
const stream: types.AudioStream = base;
for (const split of splits) {
// match codec
if (split.match(/Audio:/)) {
Object.assign(stream, {
codec: split
.match(/Audio:\W*[a-z0-9_-]*\W/i)
?.at(0)
?.replace(/Audio:/, '')
?.trim(),
});
}
// match samle rate unit
if (split.match(/hz/i)) {
Object.assign(stream, {
sampleRate: parseInt(split.replace(/[\D]/gm, '')),
});
}
}
data.streams.audio.push(stream);
}
}
};
Loading

0 comments on commit 8cb7279

Please sign in to comment.