Add util and rewrite examples (WIP)
@ -40,10 +40,8 @@ FROM ffmpeg-builder AS ffmpeg-wasm-builder
|
|||||||
COPY src/bind /src/wasm/bind
|
COPY src/bind /src/wasm/bind
|
||||||
COPY src/fftools /src/wasm/fftools
|
COPY src/fftools /src/wasm/fftools
|
||||||
COPY build/ffmpeg-wasm.sh build.sh
|
COPY build/ffmpeg-wasm.sh build.sh
|
||||||
RUN mkdir -p /src/dist/commonjs
|
RUN mkdir -p /src/dist/cjs && bash -x /src/build.sh -o dist/cjs/ffmpeg.js
|
||||||
RUN bash -x /src/build.sh -o dist/commonjs/ffmpeg.js
|
RUN mkdir -p /src/dist/esm && bash -x /src/build.sh -sEXPORT_ES6 -o dist/esm/ffmpeg.js
|
||||||
RUN mkdir -p /src/dist/esm
|
|
||||||
RUN bash -x /src/build.sh -sEXPORT_ES6 -o dist/esm/ffmpeg.js
|
|
||||||
|
|
||||||
# Export ffmpeg-core.wasm to dist/, use `docker buildx build -o . .` to get assets
|
# Export ffmpeg-core.wasm to dist/, use `docker buildx build -o . .` to get assets
|
||||||
FROM scratch AS exportor
|
FROM scratch AS exportor
|
||||||
|
10
apps/browser/package.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "browser",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "browser example",
|
||||||
|
"scripts": {
|
||||||
|
"start": "npx http-server ../../"
|
||||||
|
},
|
||||||
|
"author": "Jerome Wu <jeromewus@gmail.com>",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
10
apps/browser/style.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
35
apps/browser/transcode.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3>Upload a video to transcode to mp4 (x264) and play!</h3>
|
||||||
|
<video id="output-video" controls></video><br/>
|
||||||
|
<input type="file" id="uploader">
|
||||||
|
<p id="message"></p>
|
||||||
|
<script type="module">
|
||||||
|
import createFFmpeg from "../../packages/ffmpeg/dist/esm/ffmpeg.js";
|
||||||
|
import { fetchFile } from "../../packages/util/dist/esm/index.js";
|
||||||
|
let ffmpeg = null;
|
||||||
|
|
||||||
|
const transcode = async ({ target: { files } }) => {
|
||||||
|
const message = document.getElementById('message');
|
||||||
|
if (ffmpeg === null) {
|
||||||
|
ffmpeg = await createFFmpeg();
|
||||||
|
ffmpeg.setProgress((progress) => { console.log(progress * 100); });
|
||||||
|
}
|
||||||
|
const { name } = files[0];
|
||||||
|
ffmpeg.FS.writeFile(name, await fetchFile(files[0]));
|
||||||
|
message.innerHTML = 'Start transcoding';
|
||||||
|
await ffmpeg.exec(['-i', name, 'output.mp4']);
|
||||||
|
message.innerHTML = 'Complete transcoding';
|
||||||
|
const data = ffmpeg.FS.readFile('output.mp4');
|
||||||
|
|
||||||
|
const video = document.getElementById('output-video');
|
||||||
|
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
|
||||||
|
}
|
||||||
|
const elm = document.getElementById('uploader');
|
||||||
|
elm.addEventListener('change', transcode);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
1442
package-lock.json
generated
@ -1,9 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
|
||||||
"lint": "eslint src/bind"
|
|
||||||
},
|
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"apps/*"
|
"apps/*"
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
"name": "@ffmpeg/ffmpeg-mt",
|
"name": "@ffmpeg/ffmpeg-mt",
|
||||||
"version": "0.11.5",
|
"version": "0.11.5",
|
||||||
"description": "FFmpeg WebAssembly version w/ multi-thread",
|
"description": "FFmpeg WebAssembly version w/ multi-thread",
|
||||||
"main": "./dist/commonjs/ffmpeg.js",
|
"main": "./dist/cjs/ffmpeg.js",
|
||||||
"types": "./types/ffmpeg.d.ts",
|
"types": "./types/ffmpeg.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./types/ffmpeg.d.ts",
|
"types": "./types/ffmpeg.d.ts",
|
||||||
"import": "./dist/esm/ffmpeg.js",
|
"import": "./dist/esm/ffmpeg.js",
|
||||||
"require": "./dist/commonjs/ffmpeg.js"
|
"require": "./dist/cjs/ffmpeg.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<div id="mocha"></div>
|
<div id="mocha"></div>
|
||||||
<script src="../../../node_modules/mocha/mocha.js"></script>
|
<script src="../../../node_modules/mocha/mocha.js"></script>
|
||||||
<script src="../../../node_modules/chai/chai.js"></script>
|
<script src="../../../node_modules/chai/chai.js"></script>
|
||||||
<script src="../dist/commonjs/ffmpeg.js"></script>
|
<script src="../dist/cjs/ffmpeg.js"></script>
|
||||||
<script src="./constants.js"></script>
|
<script src="./constants.js"></script>
|
||||||
<script>mocha.setup('bdd');</script>
|
<script>mocha.setup('bdd');</script>
|
||||||
<script src="./ffmpeg.test.js"></script>
|
<script src="./ffmpeg.test.js"></script>
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
"name": "@ffmpeg/ffmpeg",
|
"name": "@ffmpeg/ffmpeg",
|
||||||
"version": "0.11.5",
|
"version": "0.11.5",
|
||||||
"description": "FFmpeg WebAssembly version",
|
"description": "FFmpeg WebAssembly version",
|
||||||
"main": "./dist/commonjs/ffmpeg.js",
|
"main": "./dist/cjs/ffmpeg.js",
|
||||||
"types": "./types/ffmpeg.d.ts",
|
"types": "./types/ffmpeg.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./types/ffmpeg.d.ts",
|
"types": "./types/ffmpeg.d.ts",
|
||||||
"import": "./dist/esm/ffmpeg.js",
|
"import": "./dist/esm/ffmpeg.js",
|
||||||
"require": "./dist/commonjs/ffmpeg.js"
|
"require": "./dist/cjs/ffmpeg.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<div id="mocha"></div>
|
<div id="mocha"></div>
|
||||||
<script src="../../../node_modules/mocha/mocha.js"></script>
|
<script src="../../../node_modules/mocha/mocha.js"></script>
|
||||||
<script src="../../../node_modules/chai/chai.js"></script>
|
<script src="../../../node_modules/chai/chai.js"></script>
|
||||||
<script src="../dist/commonjs/ffmpeg.js"></script>
|
<script src="../dist/cjs/ffmpeg.js"></script>
|
||||||
<script src="./constants.js"></script>
|
<script src="./constants.js"></script>
|
||||||
<script>mocha.setup('bdd');</script>
|
<script>mocha.setup('bdd');</script>
|
||||||
<script src="./ffmpeg.test.js"></script>
|
<script src="./ffmpeg.test.js"></script>
|
||||||
|
1
packages/node-util/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist
|
13
packages/util/.eslintrc.cjs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
|
],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ["../../tsconfig.json"],
|
||||||
|
},
|
||||||
|
plugins: ["@typescript-eslint"],
|
||||||
|
};
|
1
packages/util/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist/
|
@ -1,57 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<script src="/dist/ffmpeg.dev.js"></script>
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h3>Upload a video to transcode to mp4 (x264) and play!</h3>
|
|
||||||
<video id="output-video" controls></video><br/>
|
|
||||||
<input type="file" id="uploader">
|
|
||||||
<button onClick="cancel()">Cancel</button>
|
|
||||||
<p id="message"></p>
|
|
||||||
<script>
|
|
||||||
const { createFFmpeg, fetchFile } = FFmpeg;
|
|
||||||
let ffmpeg = null;
|
|
||||||
|
|
||||||
const transcode = async ({ target: { files } }) => {
|
|
||||||
if (ffmpeg === null) {
|
|
||||||
ffmpeg = createFFmpeg({ log: true });
|
|
||||||
}
|
|
||||||
const message = document.getElementById('message');
|
|
||||||
const { name } = files[0];
|
|
||||||
message.innerHTML = 'Loading ffmpeg-core.js';
|
|
||||||
if (!ffmpeg.isLoaded()) {
|
|
||||||
await ffmpeg.load();
|
|
||||||
}
|
|
||||||
ffmpeg.FS('writeFile', name, await fetchFile(files[0]));
|
|
||||||
message.innerHTML = 'Start transcoding';
|
|
||||||
await ffmpeg.run('-i', name, 'output.mp4');
|
|
||||||
message.innerHTML = 'Complete transcoding';
|
|
||||||
const data = ffmpeg.FS('readFile', 'output.mp4');
|
|
||||||
|
|
||||||
const video = document.getElementById('output-video');
|
|
||||||
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
|
|
||||||
}
|
|
||||||
const elm = document.getElementById('uploader');
|
|
||||||
elm.addEventListener('change', transcode);
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
try {
|
|
||||||
ffmpeg.exit();
|
|
||||||
} catch(e) {}
|
|
||||||
ffmpeg = null;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
54
packages/util/package.json
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "@ffmpeg/util",
|
||||||
|
"version": "0.11.5",
|
||||||
|
"description": "browser utils for @ffmpeg/*",
|
||||||
|
"main": "./dist/cjs/index.js",
|
||||||
|
"types": "./dist/cjs/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/cjs/index.d.ts",
|
||||||
|
"import": "./dist/esm/index.js",
|
||||||
|
"require": "./dist/cjs/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsc -p tsconfig-esm.json --watch",
|
||||||
|
"build": "rimraf dist && tsc -p tsconfig-esm.json && tsc -p tsconfig-cjs.json",
|
||||||
|
"lint": "eslint src"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/ffmpegwasm/ffmpeg.wasm.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"ffmpeg",
|
||||||
|
"video",
|
||||||
|
"audio",
|
||||||
|
"transcode"
|
||||||
|
],
|
||||||
|
"author": "Jerome Wu <jeromewus@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/ffmpegwasm/ffmpeg.wasm/issues"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.6.0"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/ffmpegwasm/ffmpeg.wasm#readme",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.37.0",
|
||||||
|
"@typescript-eslint/parser": "^5.37.0",
|
||||||
|
"eslint": "^8.23.1",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"ts-loader": "^9.4.1",
|
||||||
|
"typescript": "^4.8.3",
|
||||||
|
"webpack": "^5.74.0",
|
||||||
|
"webpack-cli": "^4.10.0"
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +0,0 @@
|
|||||||
import pkg from '../../package.json';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Default options for browser environment
|
|
||||||
*/
|
|
||||||
const corePath = typeof process !== 'undefined' && process.env.NODE_ENV === 'development'
|
|
||||||
? new URL('/node_modules/@ffmpeg/core/dist/ffmpeg-core.js', import.meta.url).href
|
|
||||||
: `https://unpkg.com/@ffmpeg/core@${pkg.devDependencies['@ffmpeg/core'].substring(1)}/dist/ffmpeg-core.js`;
|
|
||||||
|
|
||||||
export default { corePath };
|
|
@ -1,38 +0,0 @@
|
|||||||
const readFromBlobOrFile = (blob) => (
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const fileReader = new FileReader();
|
|
||||||
fileReader.onload = () => {
|
|
||||||
resolve(fileReader.result);
|
|
||||||
};
|
|
||||||
fileReader.onerror = ({ target: { error: { code } } }) => {
|
|
||||||
reject(Error(`File could not be read! Code=${code}`));
|
|
||||||
};
|
|
||||||
fileReader.readAsArrayBuffer(blob);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
export const fetchFile = async (_data) => {
|
|
||||||
let data = _data;
|
|
||||||
if (typeof _data === 'undefined') {
|
|
||||||
return new Uint8Array();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof _data === 'string') {
|
|
||||||
/* From base64 format */
|
|
||||||
if (/data:_data\/([a-zA-Z]*);base64,([^"]*)/.test(_data)) {
|
|
||||||
data = atob(_data.split(',')[1])
|
|
||||||
.split('')
|
|
||||||
.map((c) => c.charCodeAt(0));
|
|
||||||
/* From remote server/URL */
|
|
||||||
} else {
|
|
||||||
const res = await fetch(new URL(_data, import.meta.url).href);
|
|
||||||
data = await res.arrayBuffer();
|
|
||||||
}
|
|
||||||
/* From Blob or File */
|
|
||||||
} else if (_data instanceof File || _data instanceof Blob) {
|
|
||||||
data = await readFromBlobOrFile(_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Uint8Array(data);
|
|
||||||
};
|
|
@ -1,114 +0,0 @@
|
|||||||
/* eslint-disable no-undef */
|
|
||||||
import { log } from '../utils/log';
|
|
||||||
import {
|
|
||||||
CREATE_FFMPEG_CORE_IS_NOT_DEFINED,
|
|
||||||
} from '../utils/errors';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Fetch data from remote URL and convert to blob URL
|
|
||||||
* to avoid CORS issue
|
|
||||||
*/
|
|
||||||
const toBlobURL = async (url, mimeType) => {
|
|
||||||
log('info', `fetch ${url}`);
|
|
||||||
const buf = await (await fetch(url)).arrayBuffer();
|
|
||||||
log('info', `${url} file size = ${buf.byteLength} bytes`);
|
|
||||||
const blob = new Blob([buf], { type: mimeType });
|
|
||||||
const blobURL = URL.createObjectURL(blob);
|
|
||||||
log('info', `${url} blob URL = ${blobURL}`);
|
|
||||||
return blobURL;
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
export const getCreateFFmpegCore = async ({
|
|
||||||
corePath: _corePath,
|
|
||||||
workerPath: _workerPath,
|
|
||||||
wasmPath: _wasmPath,
|
|
||||||
}) => {
|
|
||||||
// in Web Worker context
|
|
||||||
// eslint-disable-next-line
|
|
||||||
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
|
|
||||||
if (typeof _corePath !== 'string') {
|
|
||||||
throw Error('corePath should be a string!');
|
|
||||||
}
|
|
||||||
const coreRemotePath = new URL(_corePath, import.meta.url).href;
|
|
||||||
const corePath = await toBlobURL(
|
|
||||||
coreRemotePath,
|
|
||||||
'application/javascript',
|
|
||||||
);
|
|
||||||
const wasmPath = await toBlobURL(
|
|
||||||
_wasmPath !== undefined ? _wasmPath : coreRemotePath.replace('ffmpeg-core.js', 'ffmpeg-core.wasm'),
|
|
||||||
'application/wasm',
|
|
||||||
);
|
|
||||||
const workerPath = await toBlobURL(
|
|
||||||
_workerPath !== undefined ? _workerPath : coreRemotePath.replace('ffmpeg-core.js', 'ffmpeg-core.worker.js'),
|
|
||||||
'application/javascript',
|
|
||||||
);
|
|
||||||
if (typeof createFFmpegCore === 'undefined') {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
globalThis.importScripts(corePath);
|
|
||||||
if (typeof createFFmpegCore === 'undefined') {
|
|
||||||
throw Error(CREATE_FFMPEG_CORE_IS_NOT_DEFINED(coreRemotePath));
|
|
||||||
}
|
|
||||||
log('info', 'ffmpeg-core.js script loaded');
|
|
||||||
resolve({
|
|
||||||
createFFmpegCore,
|
|
||||||
corePath,
|
|
||||||
wasmPath,
|
|
||||||
workerPath,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
log('info', 'ffmpeg-core.js script is loaded already');
|
|
||||||
return Promise.resolve({
|
|
||||||
createFFmpegCore,
|
|
||||||
corePath,
|
|
||||||
wasmPath,
|
|
||||||
workerPath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (typeof _corePath !== 'string') {
|
|
||||||
throw Error('corePath should be a string!');
|
|
||||||
}
|
|
||||||
const coreRemotePath = new URL(_corePath, import.meta.url).href;
|
|
||||||
const corePath = await toBlobURL(
|
|
||||||
coreRemotePath,
|
|
||||||
'application/javascript',
|
|
||||||
);
|
|
||||||
const wasmPath = await toBlobURL(
|
|
||||||
coreRemotePath.replace('ffmpeg-core.js', 'ffmpeg-core.wasm'),
|
|
||||||
'application/wasm',
|
|
||||||
);
|
|
||||||
const workerPath = await toBlobURL(
|
|
||||||
coreRemotePath.replace('ffmpeg-core.js', 'ffmpeg-core.worker.js'),
|
|
||||||
'application/javascript',
|
|
||||||
);
|
|
||||||
if (typeof createFFmpegCore === 'undefined') {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
const eventHandler = () => {
|
|
||||||
script.removeEventListener('load', eventHandler);
|
|
||||||
if (typeof createFFmpegCore === 'undefined') {
|
|
||||||
throw Error(CREATE_FFMPEG_CORE_IS_NOT_DEFINED(coreRemotePath));
|
|
||||||
}
|
|
||||||
log('info', 'ffmpeg-core.js script loaded');
|
|
||||||
resolve({
|
|
||||||
createFFmpegCore,
|
|
||||||
corePath,
|
|
||||||
wasmPath,
|
|
||||||
workerPath,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
script.src = corePath;
|
|
||||||
script.type = 'text/javascript';
|
|
||||||
script.addEventListener('load', eventHandler);
|
|
||||||
document.getElementsByTagName('head')[0].appendChild(script);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
log('info', 'ffmpeg-core.js script is loaded already');
|
|
||||||
return Promise.resolve({
|
|
||||||
createFFmpegCore,
|
|
||||||
corePath,
|
|
||||||
wasmPath,
|
|
||||||
workerPath,
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,5 +0,0 @@
|
|||||||
import defaultOptions from './defaultOptions';
|
|
||||||
import { getCreateFFmpegCore } from './getCreateFFmpegCore';
|
|
||||||
import { fetchFile } from './fetchFile';
|
|
||||||
|
|
||||||
export { defaultOptions, getCreateFFmpegCore, fetchFile };
|
|
@ -1,50 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
defaultArgs: [
|
|
||||||
/* args[0] is always the binary path */
|
|
||||||
'./ffmpeg',
|
|
||||||
/* Disable interaction mode */
|
|
||||||
'-nostdin',
|
|
||||||
/* Force to override output file */
|
|
||||||
'-y',
|
|
||||||
],
|
|
||||||
baseOptions: {
|
|
||||||
/* Flag to turn on/off log messages in console */
|
|
||||||
log: false,
|
|
||||||
/*
|
|
||||||
* Custom logger to get ffmpeg.wasm output messages.
|
|
||||||
* a sample logger looks like this:
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* logger = ({ type, message }) => {
|
|
||||||
* console.log(type, message);
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* type can be one of following:
|
|
||||||
*
|
|
||||||
* info: internal workflow debug messages
|
|
||||||
* fferr: ffmpeg native stderr output
|
|
||||||
* ffout: ffmpeg native stdout output
|
|
||||||
*/
|
|
||||||
logger: () => {},
|
|
||||||
/*
|
|
||||||
* Progress handler to get current progress of ffmpeg command.
|
|
||||||
* a sample progress handler looks like this:
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* progress = ({ ratio }) => {
|
|
||||||
* console.log(ratio);
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ratio is a float number between 0 to 1.
|
|
||||||
*/
|
|
||||||
progress: () => {},
|
|
||||||
/*
|
|
||||||
* Path to find/download ffmpeg.wasm-core,
|
|
||||||
* this value should be overwriten by `defaultOptions` in
|
|
||||||
* each environment.
|
|
||||||
*/
|
|
||||||
corePath: '',
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,279 +0,0 @@
|
|||||||
const { defaultArgs, baseOptions } = require('./config');
|
|
||||||
const parseArgs = require('./utils/parseArgs');
|
|
||||||
const { defaultOptions, getCreateFFmpegCore } = require('./node');
|
|
||||||
const { version } = require('../package.json');
|
|
||||||
|
|
||||||
const NO_LOAD = Error('ffmpeg.wasm is not ready, make sure you have completed load().');
|
|
||||||
|
|
||||||
module.exports = (_options = {}) => {
|
|
||||||
const {
|
|
||||||
log: optLog,
|
|
||||||
logger,
|
|
||||||
progress: optProgress,
|
|
||||||
...options
|
|
||||||
} = {
|
|
||||||
...baseOptions,
|
|
||||||
...defaultOptions,
|
|
||||||
..._options,
|
|
||||||
};
|
|
||||||
let Core = null;
|
|
||||||
let ffmpeg = null;
|
|
||||||
let runResolve = null;
|
|
||||||
let runReject = null;
|
|
||||||
let running = false;
|
|
||||||
let customLogger = () => {};
|
|
||||||
let logging = optLog;
|
|
||||||
let progress = optProgress;
|
|
||||||
let duration = 0;
|
|
||||||
let frames = 0;
|
|
||||||
let readFrames = false;
|
|
||||||
let ratio = 0;
|
|
||||||
|
|
||||||
const detectCompletion = (message) => {
|
|
||||||
if (message === 'FFMPEG_END' && runResolve !== null) {
|
|
||||||
runResolve();
|
|
||||||
runResolve = null;
|
|
||||||
runReject = null;
|
|
||||||
running = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const log = (type, message) => {
|
|
||||||
customLogger({ type, message });
|
|
||||||
if (logging) {
|
|
||||||
console.log(`[${type}] ${message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const ts2sec = (ts) => {
|
|
||||||
const [h, m, s] = ts.split(':');
|
|
||||||
return (parseFloat(h) * 60 * 60) + (parseFloat(m) * 60) + parseFloat(s);
|
|
||||||
};
|
|
||||||
const parseProgress = (message, prog) => {
|
|
||||||
if (typeof message === 'string') {
|
|
||||||
if (message.startsWith(' Duration')) {
|
|
||||||
const ts = message.split(', ')[0].split(': ')[1];
|
|
||||||
const d = ts2sec(ts);
|
|
||||||
prog({ duration: d, ratio });
|
|
||||||
if (duration === 0 || duration > d) {
|
|
||||||
duration = d;
|
|
||||||
readFrames = true;
|
|
||||||
}
|
|
||||||
} else if (readFrames && message.startsWith(' Stream')) {
|
|
||||||
const match = message.match(/([\d.]+) fps/);
|
|
||||||
if (match) {
|
|
||||||
const fps = parseFloat(match[1]);
|
|
||||||
frames = duration * fps;
|
|
||||||
} else {
|
|
||||||
frames = 0;
|
|
||||||
}
|
|
||||||
readFrames = false;
|
|
||||||
} else if (message.startsWith('frame') || message.startsWith('size')) {
|
|
||||||
const ts = message.split('time=')[1].split(' ')[0];
|
|
||||||
const t = ts2sec(ts);
|
|
||||||
const match = message.match(/frame=\s*(\d+)/);
|
|
||||||
if (frames && match) {
|
|
||||||
const f = parseFloat(match[1]);
|
|
||||||
ratio = Math.min(f / frames, 1);
|
|
||||||
} else {
|
|
||||||
ratio = t / duration;
|
|
||||||
}
|
|
||||||
prog({ ratio, time: t });
|
|
||||||
} else if (message.startsWith('video:')) {
|
|
||||||
prog({ ratio: 1 });
|
|
||||||
duration = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const parseMessage = ({ type, message }) => {
|
|
||||||
log(type, message);
|
|
||||||
parseProgress(message, progress);
|
|
||||||
detectCompletion(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Load ffmpeg.wasm-core script.
|
|
||||||
* In browser environment, the ffmpeg.wasm-core script is fetch from
|
|
||||||
* CDN and can be assign to a local path by assigning `corePath`.
|
|
||||||
* In node environment, we use dynamic require and the default `corePath`
|
|
||||||
* is `$ffmpeg/core`.
|
|
||||||
*
|
|
||||||
* Typically the load() func might take few seconds to minutes to complete,
|
|
||||||
* better to do it as early as possible.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const load = async () => {
|
|
||||||
log('info', 'load ffmpeg-core');
|
|
||||||
if (Core === null) {
|
|
||||||
log('info', 'loading ffmpeg-core');
|
|
||||||
/*
|
|
||||||
* In node environment, all paths are undefined as there
|
|
||||||
* is no need to set them.
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
createFFmpegCore,
|
|
||||||
corePath,
|
|
||||||
workerPath,
|
|
||||||
wasmPath,
|
|
||||||
} = await getCreateFFmpegCore(options);
|
|
||||||
Core = await createFFmpegCore({
|
|
||||||
/*
|
|
||||||
* Assign mainScriptUrlOrBlob fixes chrome extension web worker issue
|
|
||||||
* as there is no document.currentScript in the context of content_scripts
|
|
||||||
*/
|
|
||||||
mainScriptUrlOrBlob: corePath,
|
|
||||||
printErr: (message) => parseMessage({ type: 'fferr', message }),
|
|
||||||
print: (message) => parseMessage({ type: 'ffout', message }),
|
|
||||||
/*
|
|
||||||
* locateFile overrides paths of files that is loaded by main script (ffmpeg-core.js).
|
|
||||||
* It is critical for browser environment and we override both wasm and worker paths
|
|
||||||
* as we are using blob URL instead of original URL to avoid cross origin issues.
|
|
||||||
*/
|
|
||||||
locateFile: (path, prefix) => {
|
|
||||||
if (typeof window !== 'undefined' || typeof WorkerGlobalScope !== 'undefined') {
|
|
||||||
if (typeof wasmPath !== 'undefined'
|
|
||||||
&& path.endsWith('ffmpeg-core.wasm')) {
|
|
||||||
return wasmPath;
|
|
||||||
}
|
|
||||||
if (typeof workerPath !== 'undefined'
|
|
||||||
&& path.endsWith('ffmpeg-core.worker.js')) {
|
|
||||||
return workerPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return prefix + path;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
ffmpeg = Core.cwrap(options.mainName || 'proxy_main', 'number', ['number', 'number']);
|
|
||||||
log('info', 'ffmpeg-core loaded');
|
|
||||||
} else {
|
|
||||||
throw Error('ffmpeg.wasm was loaded, you should not load it again, use ffmpeg.isLoaded() to check next time.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Determine whether the Core is loaded.
|
|
||||||
*/
|
|
||||||
const isLoaded = () => Core !== null;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Run ffmpeg command.
|
|
||||||
* This is the major function in ffmpeg.wasm, you can just imagine it
|
|
||||||
* as ffmpeg native cli and what you need to pass is the same.
|
|
||||||
*
|
|
||||||
* For example, you can convert native command below:
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* $ ffmpeg -i video.avi -c:v libx264 video.mp4
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* To
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* await ffmpeg.run('-i', 'video.avi', '-c:v', 'libx264', 'video.mp4');
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const run = (..._args) => {
|
|
||||||
log('info', `run ffmpeg command: ${_args.join(' ')}`);
|
|
||||||
if (Core === null) {
|
|
||||||
throw NO_LOAD;
|
|
||||||
} else if (running) {
|
|
||||||
throw Error('ffmpeg.wasm can only run one command at a time');
|
|
||||||
} else {
|
|
||||||
running = true;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const args = [...defaultArgs, ..._args].filter((s) => s.length !== 0);
|
|
||||||
runResolve = resolve;
|
|
||||||
runReject = reject;
|
|
||||||
ffmpeg(...parseArgs(Core, args));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Run FS operations.
|
|
||||||
* For input/output file of ffmpeg.wasm, it is required to save them to MEMFS
|
|
||||||
* first so that ffmpeg.wasm is able to consume them. Here we rely on the FS
|
|
||||||
* methods provided by Emscripten.
|
|
||||||
*
|
|
||||||
* Common methods to use are:
|
|
||||||
* ffmpeg.FS('writeFile', 'video.avi', new Uint8Array(...)): writeFile writes
|
|
||||||
* data to MEMFS. You need to use Uint8Array for binary data.
|
|
||||||
* ffmpeg.FS('readFile', 'video.mp4'): readFile from MEMFS.
|
|
||||||
* ffmpeg.FS('unlink', 'video.map'): delete file from MEMFS.
|
|
||||||
*
|
|
||||||
* For more info, check https://emscripten.org/docs/api_reference/Filesystem-API.html
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const FS = (method, ...args) => {
|
|
||||||
log('info', `run FS.${method} ${args.map((arg) => (typeof arg === 'string' ? arg : `<${arg.length} bytes binary file>`)).join(' ')}`);
|
|
||||||
if (Core === null) {
|
|
||||||
throw NO_LOAD;
|
|
||||||
} else {
|
|
||||||
let ret = null;
|
|
||||||
try {
|
|
||||||
ret = Core.FS[method](...args);
|
|
||||||
} catch (e) {
|
|
||||||
if (method === 'readdir') {
|
|
||||||
throw Error(`ffmpeg.FS('readdir', '${args[0]}') error. Check if the path exists, ex: ffmpeg.FS('readdir', '/')`);
|
|
||||||
} else if (method === 'readFile') {
|
|
||||||
throw Error(`ffmpeg.FS('readFile', '${args[0]}') error. Check if the path exists`);
|
|
||||||
} else {
|
|
||||||
throw Error('Oops, something went wrong in FS operation.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* forcibly terminate the ffmpeg program.
|
|
||||||
*/
|
|
||||||
const exit = () => {
|
|
||||||
if (Core === null) {
|
|
||||||
throw NO_LOAD;
|
|
||||||
} else {
|
|
||||||
// if there's any pending runs, reject them
|
|
||||||
if (runReject) {
|
|
||||||
runReject('ffmpeg has exited');
|
|
||||||
}
|
|
||||||
running = false;
|
|
||||||
try {
|
|
||||||
Core.exit(1);
|
|
||||||
} catch (err) {
|
|
||||||
log(err.message);
|
|
||||||
if (runReject) {
|
|
||||||
runReject(err);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
Core = null;
|
|
||||||
ffmpeg = null;
|
|
||||||
runResolve = null;
|
|
||||||
runReject = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setProgress = (_progress) => {
|
|
||||||
progress = _progress;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setLogger = (_logger) => {
|
|
||||||
customLogger = _logger;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setLogging = (_logging) => {
|
|
||||||
logging = _logging;
|
|
||||||
};
|
|
||||||
|
|
||||||
log('info', `use ffmpeg.wasm v${version}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
setProgress,
|
|
||||||
setLogger,
|
|
||||||
setLogging,
|
|
||||||
load,
|
|
||||||
isLoaded,
|
|
||||||
run,
|
|
||||||
exit,
|
|
||||||
FS,
|
|
||||||
};
|
|
||||||
};
|
|
120
packages/util/src/index.d.ts
vendored
@ -1,120 +0,0 @@
|
|||||||
export const FS: {
|
|
||||||
writeFile: (fileName: string, binaryData: Uint8Array | string) => void,
|
|
||||||
readFile: (fileName: string) => Uint8Array,
|
|
||||||
readdir: (pathName: string) => string[],
|
|
||||||
unlink: (fileName: string) => void,
|
|
||||||
mkdir: (fileName: string) => void,
|
|
||||||
}
|
|
||||||
|
|
||||||
type FSMethodNames = { [K in keyof typeof FS]: (typeof FS)[K] extends (...args: any[]) => any ? K : never }[keyof typeof FS];
|
|
||||||
type FSMethodArgs = { [K in FSMethodNames]: Parameters<(typeof FS)[K]> };
|
|
||||||
type FSMethodReturn = { [K in FSMethodNames]: ReturnType<(typeof FS)[K]> };
|
|
||||||
|
|
||||||
type LogCallback = (logParams: { type: string; message: string }) => any;
|
|
||||||
type ProgressCallback = (progressParams: { ratio: number }) => any;
|
|
||||||
|
|
||||||
export interface CreateFFmpegOptions {
|
|
||||||
/** path for ffmpeg-core.js script */
|
|
||||||
corePath?: string;
|
|
||||||
/** path for ffmpeg-worker.js script */
|
|
||||||
workerPath?: string;
|
|
||||||
/** path for ffmpeg-core.wasm script */
|
|
||||||
wasmPath?: string;
|
|
||||||
/** a boolean to turn on all logs, default is false */
|
|
||||||
log?: boolean;
|
|
||||||
/** a function to get log messages, a quick example is ({ message }) => console.log(message) */
|
|
||||||
logger?: LogCallback;
|
|
||||||
/** a function to trace the progress, a quick example is p => console.log(p) */
|
|
||||||
progress?: ProgressCallback;
|
|
||||||
/** name of the main function of the ffmpeg-core.js script */
|
|
||||||
mainName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FFmpeg {
|
|
||||||
/*
|
|
||||||
* Load ffmpeg.wasm-core script.
|
|
||||||
* In browser environment, the ffmpeg.wasm-core script is fetch from
|
|
||||||
* CDN and can be assign to a local path by assigning `corePath`.
|
|
||||||
* In node environment, we use dynamic require and the default `corePath`
|
|
||||||
* is `$ffmpeg/core`.
|
|
||||||
*
|
|
||||||
* Typically the load() func might take few seconds to minutes to complete,
|
|
||||||
* better to do it as early as possible.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
load(): Promise<void>;
|
|
||||||
/*
|
|
||||||
* Determine whether the Core is loaded.
|
|
||||||
*/
|
|
||||||
isLoaded(): boolean;
|
|
||||||
/*
|
|
||||||
* Run ffmpeg command.
|
|
||||||
* This is the major function in ffmpeg.wasm, you can just imagine it
|
|
||||||
* as ffmpeg native cli and what you need to pass is the same.
|
|
||||||
*
|
|
||||||
* For example, you can convert native command below:
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* $ ffmpeg -i video.avi -c:v libx264 video.mp4
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* To
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* await ffmpeg.run('-i', 'video.avi', '-c:v', 'libx264', 'video.mp4');
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
run(...args: string[]): Promise<void>;
|
|
||||||
/*
|
|
||||||
* Run FS operations.
|
|
||||||
* For input/output file of ffmpeg.wasm, it is required to save them to MEMFS
|
|
||||||
* first so that ffmpeg.wasm is able to consume them. Here we rely on the FS
|
|
||||||
* methods provided by Emscripten.
|
|
||||||
*
|
|
||||||
* Common methods to use are:
|
|
||||||
* ffmpeg.FS('writeFile', 'video.avi', new Uint8Array(...)): writeFile writes
|
|
||||||
* data to MEMFS. You need to use Uint8Array for binary data.
|
|
||||||
* ffmpeg.FS('readFile', 'video.mp4'): readFile from MEMFS.
|
|
||||||
* ffmpeg.FS('unlink', 'video.map'): delete file from MEMFS.
|
|
||||||
*
|
|
||||||
* For more info, check https://emscripten.org/docs/api_reference/Filesystem-API.html
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
FS<Method extends FSMethodNames>(method: Method, ...args: FSMethodArgs[Method]): FSMethodReturn[Method];
|
|
||||||
setProgress(progress: ProgressCallback): void;
|
|
||||||
setLogger(log: LogCallback): void;
|
|
||||||
setLogging(logging: boolean): void;
|
|
||||||
exit(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Create ffmpeg instance.
|
|
||||||
* Each ffmpeg instance owns an isolated MEMFS and works
|
|
||||||
* independently.
|
|
||||||
*
|
|
||||||
* For example:
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* const ffmpeg = createFFmpeg({
|
|
||||||
* log: true,
|
|
||||||
* logger: () => {},
|
|
||||||
* progress: () => {},
|
|
||||||
* corePath: '',
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* For the usage of these four arguments, check config.js
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function createFFmpeg(options?: CreateFFmpegOptions): FFmpeg;
|
|
||||||
/*
|
|
||||||
* Helper function for fetching files from various resource.
|
|
||||||
* Sometimes the video/audio file you want to process may located
|
|
||||||
* in a remote URL and somewhere in your local file system.
|
|
||||||
*
|
|
||||||
* This helper function helps you to fetch to file and return an
|
|
||||||
* Uint8Array variable for ffmpeg.wasm to consume.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function fetchFile(data: string | Buffer | Blob | File): Promise<Uint8Array>;
|
|
@ -1,36 +0,0 @@
|
|||||||
require('regenerator-runtime/runtime');
|
|
||||||
const createFFmpeg = require('./createFFmpeg');
|
|
||||||
const { fetchFile } = require('./node');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
/*
|
|
||||||
* Create ffmpeg instance.
|
|
||||||
* Each ffmpeg instance owns an isolated MEMFS and works
|
|
||||||
* independently.
|
|
||||||
*
|
|
||||||
* For example:
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* const ffmpeg = createFFmpeg({
|
|
||||||
* log: true,
|
|
||||||
* logger: () => {},
|
|
||||||
* progress: () => {},
|
|
||||||
* corePath: '',
|
|
||||||
* })
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* For the usage of these four arguments, check config.js
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
createFFmpeg,
|
|
||||||
/*
|
|
||||||
* Helper function for fetching files from various resource.
|
|
||||||
* Sometimes the video/audio file you want to process may located
|
|
||||||
* in a remote URL and somewhere in your local file system.
|
|
||||||
*
|
|
||||||
* This helper function helps you to fetch to file and return an
|
|
||||||
* Uint8Array variable for ffmpeg.wasm to consume.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
fetchFile,
|
|
||||||
};
|
|
107
packages/util/src/index.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
export const readFromBlobOrFile = (blob: Blob | File): Promise<Uint8Array> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onload = () => {
|
||||||
|
const { result } = fileReader;
|
||||||
|
if (result instanceof ArrayBuffer) {
|
||||||
|
resolve(new Uint8Array(result));
|
||||||
|
} else {
|
||||||
|
resolve(new Uint8Array());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fileReader.onerror = (event) => {
|
||||||
|
reject(
|
||||||
|
Error(
|
||||||
|
`File could not be read! Code=${event?.target?.error?.code || -1}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
fileReader.readAsArrayBuffer(blob);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An util function to fetch data from url string, base64, URL, File or Blob format.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* ```ts
|
||||||
|
* // URL
|
||||||
|
* await fetchFile("http://localhost:3000/video.mp4");
|
||||||
|
* // base64
|
||||||
|
* await fetchFile("data:<type>;base64,wL2dvYWwgbW9yZ...");
|
||||||
|
* // URL
|
||||||
|
* await fetchFile(new URL("video.mp4", import.meta.url));
|
||||||
|
* // File
|
||||||
|
* fileInput.addEventListener('change', (e) => {
|
||||||
|
* await fetchFile(e.target.files[0]);
|
||||||
|
* });
|
||||||
|
* // Blob
|
||||||
|
* const blob = new Blob(...);
|
||||||
|
* await fetchFile(blob);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const fetchFile = async (
|
||||||
|
file?: string | File | Blob
|
||||||
|
): Promise<Uint8Array> => {
|
||||||
|
let data: ArrayBuffer | number[];
|
||||||
|
|
||||||
|
if (typeof file === "string") {
|
||||||
|
/* From base64 format */
|
||||||
|
if (/data:_data\/([a-zA-Z]*);base64,([^"]*)/.test(file)) {
|
||||||
|
data = atob(file.split(",")[1])
|
||||||
|
.split("")
|
||||||
|
.map((c) => c.charCodeAt(0));
|
||||||
|
/* From remote server/URL */
|
||||||
|
} else {
|
||||||
|
data = await (await fetch(file)).arrayBuffer();
|
||||||
|
}
|
||||||
|
} else if (file instanceof URL) {
|
||||||
|
data = await (await fetch(file)).arrayBuffer();
|
||||||
|
} else if (file instanceof File || file instanceof Blob) {
|
||||||
|
data = await readFromBlobOrFile(file);
|
||||||
|
} else {
|
||||||
|
return new Uint8Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* importScript dynamically import a script, useful when you
|
||||||
|
* want to use different versions of ffmpeg.wasm based on environment.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* await importScript("http://localhost:3000/ffmpeg.js");
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const importScript = async (url: string): Promise<void> =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
const eventHandler = () => {
|
||||||
|
script.removeEventListener("load", eventHandler);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
script.src = url;
|
||||||
|
script.type = "text/javascript";
|
||||||
|
script.addEventListener("load", eventHandler);
|
||||||
|
document.getElementsByTagName("head")[0].appendChild(script);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* toBlobURL fetches data from an URL and return a blob URL.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* await toBlobURL("http://localhost:3000/ffmpeg.js", "text/javascript");
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const toBlobURL = async (
|
||||||
|
url: string,
|
||||||
|
mimeType: string
|
||||||
|
): Promise<string> => {
|
||||||
|
const buf = await (await fetch(url)).arrayBuffer();
|
||||||
|
const blob = new Blob([buf], { type: mimeType });
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
};
|