Reorg folders and use core as ffmpeg from now on

This commit is contained in:
Jerome Wu
2022-09-22 13:06:44 +08:00
parent 4f03229810
commit 20790e4fd2
138 changed files with 10505 additions and 1223 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,22 @@
Browser Examples
==================
To run this example, execute:
```
$ npm start
```
Visit http://localhost:3000/examples/browser/transcode.html
Web Worker Examples
==================
To run the webworker example, execute:
```
$ npm run start:worker
```
Visit http://localhost:3000/examples/browser/transcode.worker.html

View File

@@ -0,0 +1,55 @@
<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>Select multiple video files to Concatenate</h3>
<video id="output-video" controls></video><br />
<input type="file" id="uploader" multiple />
<p id="message"></p>
<script>
const { createFFmpeg, fetchFile } = FFmpeg;
const ffmpeg = createFFmpeg({ log: true });
const transcode = async ({ target: { files } }) => {
const message = document.getElementById("message");
message.innerHTML = "Loading ffmpeg-core.js";
await ffmpeg.load();
message.innerHTML = "Start Concating";
const inputPaths = [];
for (const file of files) {
const { name } = file;
ffmpeg.FS('writeFile', name, await fetchFile(file));
inputPaths.push(`file ${name}`);
}
ffmpeg.FS('writeFile', 'concat_list.txt', inputPaths.join('\n'));
await ffmpeg.run('-f', 'concat', '-safe', '0', '-i', 'concat_list.txt', 'output.mp4');
message.innerHTML = "Complete Concating";
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>

View File

@@ -0,0 +1,53 @@
<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>Click start to transcode images to mp4 (x264) and play!</h3>
<video id="output-video" controls></video><br/>
<button id="start-btn">Start</button>
<p id="message"></p>
<a href="https://github.com/ffmpegjs/ffmpeg.js/tree/master/examples/assets/triangle">Data Set</a>
<script>
const { createFFmpeg, fetchFile } = FFmpeg;
const ffmpeg = createFFmpeg({ log: true });
const image2video = async () => {
const message = document.getElementById('message');
message.innerHTML = 'Loading ffmpeg-core.js';
await ffmpeg.load();
message.innerHTML = 'Loading data';
ffmpeg.FS('writeFile', 'audio.ogg', await fetchFile('../assets/triangle/audio.ogg'));
for (let i = 0; i < 60; i += 1) {
const num = `00${i}`.slice(-3);
ffmpeg.FS('writeFile', `tmp.${num}.png`, await fetchFile(`../assets/triangle/tmp.${num}.png`));
}
message.innerHTML = 'Start transcoding';
await ffmpeg.run('-framerate', '30', '-pattern_type', 'glob', '-i', '*.png', '-i', 'audio.ogg', '-c:a', 'copy', '-shortest', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', 'out.mp4');
const data = ffmpeg.FS('readFile', 'out.mp4');
ffmpeg.FS('unlink', 'audio.ogg')
for (let i = 0; i < 60; i += 1) {
const num = `00${i}`.slice(-3);
ffmpeg.FS('unlink', `tmp.${num}.png`);
}
const video = document.getElementById('output-video');
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
}
const elm = document.getElementById('start-btn');
elm.addEventListener('click', image2video);
</script>
</body>
</html>

View File

@@ -0,0 +1,57 @@
<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>

View File

@@ -0,0 +1,48 @@
<html>
<head>
<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">
<p id="message"></p>
<script type="module">
const worker = new Worker(new URL('./transcode.worker.js', import.meta.url).href);
worker.onmessage = (event) => {
const {data} = event;
message.innerHTML = 'Complete transcoding';
const video = document.getElementById('output-video');
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
}
worker.onerror = (error) => console.log(error);
const transcode = async ({ target: { files } }) => {
const message = document.getElementById('message');
const [file] = files;
let name = file.name.split('.');
const inType = name.pop();
name = name.join();
const buffer = await file.arrayBuffer();
const outType = 'mp4';
worker.postMessage({name, inType, outType, buffer}, [buffer]);
message.innerHTML = 'Start transcoding';
}
const elm = document.getElementById('uploader');
elm.addEventListener('change', transcode);
</script>
</body>
</html>

View File

@@ -0,0 +1,24 @@
importScripts('/dist/ffmpeg.dev.js');
const ffmpeg = self.FFmpeg.createFFmpeg({log: true});
onmessage = async (event) => {
try {
const {buffer, name, inType, outType} = event.data;
if (!ffmpeg.isLoaded()) {
await ffmpeg.load();
}
ffmpeg.FS('writeFile', `${name}.${inType}`, new Uint8Array(buffer));
await ffmpeg.run('-i', `${name}.${inType}`, `${name}.${outType}`);
const data = ffmpeg.FS('readFile', `${name}.${outType}`);
postMessage({buffer: data.buffer, type: "result"}, [data.buffer]);
// delete files from memory
ffmpeg.FS('unlink', `${name}.${inType}`);
ffmpeg.FS('unlink', `${name}.${outType}`);
} catch (e) {
postMessage({type: "error", error: e});
}
}

View File

@@ -0,0 +1,44 @@
<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 mp4 (x264) video and trim its first 1 seconds and play!</h3>
<video id="output-video" controls></video><br/>
<input type="file" id="uploader">
<p id="message"></p>
<script>
const { createFFmpeg, fetchFile } = FFmpeg;
const ffmpeg = createFFmpeg({ log: true });
const trim = async ({ target: { files } }) => {
const message = document.getElementById('message');
const { name } = files[0];
message.innerHTML = 'Loading ffmpeg-core.js';
await ffmpeg.load();
message.innerHTML = 'Start trimming';
ffmpeg.FS('writeFile', name, await fetchFile(files[0]));
await ffmpeg.run('-i', name, '-ss', '0', '-to', '1', 'output.mp4');
message.innerHTML = 'Complete trimming';
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', trim);
</script>
</body>
</html>

View File

@@ -0,0 +1,72 @@
<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>Record video from webcam and transcode to mp4 (x264) and play!</h3>
<div>
<video id="webcam" width="320px" height="180px"></video>
<video id="output-video" width="320px" height="180px" controls></video>
</div>
<button id="record" disabled>Start Recording</button>
<p id="message"></p>
<script>
const { createFFmpeg, fetchFile } = FFmpeg;
const ffmpeg = createFFmpeg({ log: true });
const webcam = document.getElementById('webcam');
const recordBtn = document.getElementById('record');
const startRecording = () => {
const rec = new MediaRecorder(webcam.srcObject);
const chunks = [];
recordBtn.textContent = 'Stop Recording';
recordBtn.onclick = () => {
rec.stop();
recordBtn.textContent = 'Start Recording';
recordBtn.onclick = startRecording;
}
rec.ondataavailable = e => chunks.push(e.data);
rec.onstop = async () => {
transcode(new Uint8Array(await (new Blob(chunks)).arrayBuffer()));
};
rec.start();
};
(async () => {
webcam.srcObject = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
await webcam.play();
recordBtn.disabled = false;
recordBtn.onclick = startRecording;
})();
const transcode = async (webcamData) => {
const message = document.getElementById('message');
const name = 'record.webm';
message.innerHTML = 'Loading ffmpeg-core.js';
await ffmpeg.load();
message.innerHTML = 'Start transcoding';
ffmpeg.FS('writeFile', name, await fetchFile(webcamData));
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' }));
}
</script>
</body>
</html>

View File

@@ -0,0 +1,13 @@
const fs = require('fs');
const { createFFmpeg, fetchFile } = require('../../src');
const ffmpeg = createFFmpeg({ log: true });
(async () => {
await ffmpeg.load();
ffmpeg.FS('writeFile', 'flame.avi', await fetchFile('../assets/flame.avi'));
ffmpeg.FS('writeFile', 'concat_list.txt', 'file flame.avi\nfile flame.avi');
await ffmpeg.run('-f', 'concat', '-safe', '0', '-i', 'concat_list.txt', 'flame.mp4');
await fs.promises.writeFile('flame.mp4', ffmpeg.FS('readFile', 'flame.mp4'));
process.exit(0);
})();

View File

@@ -0,0 +1,12 @@
const fs = require('fs');
const { createFFmpeg, fetchFile } = require('../../src');
const ffmpeg = createFFmpeg({ log: true });
(async () => {
await ffmpeg.load();
ffmpeg.FS('writeFile', 'flame.avi', await fetchFile('../assets/flame.avi'));
await ffmpeg.run('-i', 'flame.avi', '-i', 'flame.avi', '-filter_complex', 'hstack', 'flame.mp4');
await fs.promises.writeFile('flame.mp4', ffmpeg.FS('readFile', 'flame.mp4'));
process.exit(0);
})();

View File

@@ -0,0 +1,22 @@
const fs = require('fs');
const { createFFmpeg, fetchFile } = require('../../src');
const ffmpeg = createFFmpeg({ log: true });
(async () => {
await ffmpeg.load();
ffmpeg.FS('writeFile', 'audio.ogg', await fetchFile('../assets/triangle/audio.ogg'));
for (let i = 0; i < 60; i += 1) {
const num = `00${i}`.slice(-3);
ffmpeg.FS('writeFile', `tmp.${num}.png`, await fetchFile(`../assets/triangle/tmp.${num}.png`));
}
console.log(ffmpeg.FS('readdir', '/'));
await ffmpeg.run('-framerate', '30', '-pattern_type', 'glob', '-i', '*.png', '-i', 'audio.ogg', '-c:a', 'copy', '-shortest', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', 'out.mp4');
await ffmpeg.FS('unlink', 'audio.ogg');
for (let i = 0; i < 60; i += 1) {
const num = `00${i}`.slice(-3);
await ffmpeg.FS('unlink', `tmp.${num}.png`);
}
await fs.promises.writeFile('out.mp4', ffmpeg.FS('readFile', 'out.mp4'));
process.exit(0);
})();

View File

@@ -0,0 +1,14 @@
const fs = require('fs');
const { createFFmpeg, fetchFile } = require('../../src');
const ffmpeg = createFFmpeg({ log: true });
(async () => {
await ffmpeg.load();
ffmpeg.FS('writeFile', 'flame.avi', await fetchFile('../assets/flame.avi'));
await ffmpeg.run('-i', 'flame.avi', '-map', '0:v', '-r', '25', 'out_%06d.bmp');
ffmpeg.FS('readdir', '/').filter((p) => p.endsWith('.bmp')).forEach(async (p) => {
fs.writeFileSync(p, ffmpeg.FS('readFile', p));
});
process.exit(0);
})();

View File

@@ -0,0 +1,14 @@
const fs = require('fs');
const { createFFmpeg, fetchFile } = require('../../src');
const ffmpeg = createFFmpeg({
log: true,
});
(async () => {
await ffmpeg.load();
ffmpeg.FS('writeFile', 'flame.avi', await fetchFile('../assets/flame.avi'));
await ffmpeg.run('-i', 'flame.avi', 'flame.mp4');
await fs.promises.writeFile('flame.mp4', ffmpeg.FS('readFile', 'flame.mp4'));
process.exit(0);
})();

View File

@@ -0,0 +1,12 @@
const fs = require('fs');
const { createFFmpeg, fetchFile } = require('../../src');
const ffmpeg = createFFmpeg({ log: true });
(async () => {
await ffmpeg.load();
ffmpeg.FS('writeFile', 'flame.avi', await fetchFile('../assets/flame.avi'));
await ffmpeg.run('-i', 'flame.avi', '-ss', '0', '-to', '1', 'flame_trim.avi');
await fs.promises.writeFile('flame_trim.avi', ffmpeg.FS('readFile', 'flame_trim.avi'));
process.exit(0);
})();

View File

@@ -0,0 +1,10 @@
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 };

View File

@@ -0,0 +1,38 @@
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);
};

View File

@@ -0,0 +1,114 @@
/* 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,
});
};

View File

@@ -0,0 +1,5 @@
import defaultOptions from './defaultOptions';
import { getCreateFFmpegCore } from './getCreateFFmpegCore';
import { fetchFile } from './fetchFile';
export { defaultOptions, getCreateFFmpegCore, fetchFile };

View File

@@ -0,0 +1,50 @@
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: '',
},
};

View File

@@ -0,0 +1,279 @@
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 Normal file
View File

@@ -0,0 +1,120 @@
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>;

View File

@@ -0,0 +1,36 @@
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,
};

View File

@@ -0,0 +1,6 @@
/*
* Default options for node environment
*/
module.exports = {
corePath: '@ffmpeg/core',
};

View File

@@ -0,0 +1,33 @@
const util = require('util');
const fs = require('fs');
const fetch = require('node-fetch');
const isURL = require('is-url');
module.exports = async (_data) => {
let data = _data;
if (typeof _data === 'undefined') {
return new Uint8Array();
}
if (typeof _data === 'string') {
/* From remote URL/server */
if (isURL(_data)
|| _data.startsWith('moz-extension://')
|| _data.startsWith('chrome-extension://')
|| _data.startsWith('file://')) {
const res = await fetch(_data);
data = await res.arrayBuffer();
/* From base64 format */
} else if (/data:_data\/([a-zA-Z]*);base64,([^"]*)/.test(_data)) {
data = Buffer.from(_data.split(',')[1], 'base64');
/* From local file path */
} else {
data = await util.promisify(fs.readFile)(_data);
}
/* From Buffer */
} else if (Buffer.isBuffer(_data)) {
data = _data;
}
return new Uint8Array(data);
};

View File

@@ -0,0 +1,7 @@
const { log } = require('../utils/log');
module.exports = ({ corePath }) => new Promise((resolve) => {
log('info', `fetch ffmpeg.wasm-core script from ${corePath}`);
// eslint-disable-next-line import/no-dynamic-require
resolve({ createFFmpegCore: require(corePath) });
});

View File

@@ -0,0 +1,9 @@
const defaultOptions = require('./defaultOptions');
const getCreateFFmpegCore = require('./getCreateFFmpegCore');
const fetchFile = require('./fetchFile');
module.exports = {
defaultOptions,
getCreateFFmpegCore,
fetchFile,
};

View File

@@ -0,0 +1,11 @@
const CREATE_FFMPEG_CORE_IS_NOT_DEFINED = (corePath) => (`
createFFmpegCore is not defined. ffmpeg.wasm is unable to find createFFmpegCore after loading ffmpeg-core.js from ${corePath}. Use another URL when calling createFFmpeg():
const ffmpeg = createFFmpeg({
corePath: 'http://localhost:3000/ffmpeg-core.js',
});
`);
module.exports = {
CREATE_FFMPEG_CORE_IS_NOT_DEFINED,
};

View File

@@ -0,0 +1,24 @@
let logging = false;
let customLogger = () => {};
const setLogging = (_logging) => {
logging = _logging;
};
const setCustomLogger = (logger) => {
customLogger = logger;
};
const log = (type, message) => {
customLogger({ type, message });
if (logging) {
console.log(`[${type}] ${message}`);
}
};
module.exports = {
logging,
setLogging,
setCustomLogger,
log,
};

View File

@@ -0,0 +1,10 @@
module.exports = (Core, args) => {
const argsPtr = Core._malloc(args.length * Uint32Array.BYTES_PER_ELEMENT);
args.forEach((s, idx) => {
const sz = Core.lengthBytesUTF8(s) + 1;
const buf = Core._malloc(sz);
Core.stringToUTF8(s, buf, sz);
Core.setValue(argsPtr + (Uint32Array.BYTES_PER_ELEMENT * idx), buf, 'i32');
});
return [args.length, argsPtr];
};

View File

@@ -0,0 +1,6 @@
{
"rules": {
"no-undef": 0,
"camelcase": 0
}
}

View File

@@ -0,0 +1,19 @@
const TIMEOUT = 60000;
const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined';
const OPTIONS = {
corePath: IS_BROWSER ? 'http://localhost:3000/node_modules/@ffmpeg/core/dist/ffmpeg-core.js' : '@ffmpeg/core',
};
const FLAME_MP4_LENGTH = 100374;
const META_FLAME_MP4_LENGTH = 100408;
const META_FLAME_MP4_LENGTH_NO_SPACE = 100404;
if (typeof module !== 'undefined') {
module.exports = {
TIMEOUT,
IS_BROWSER,
OPTIONS,
FLAME_MP4_LENGTH,
META_FLAME_MP4_LENGTH,
META_FLAME_MP4_LENGTH_NO_SPACE,
};
}

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FFmpeg Unit Test</title>
<link rel="stylesheet" href="../node_modules/mocha/mocha.css">
</head>
<body>
<div id="mocha"></div>
<script src="../node_modules/mocha/mocha.js"></script>
<script src="../node_modules/chai/chai.js"></script>
<script src="../dist/ffmpeg.dev.js"></script>
<script src="./constants.js"></script>
<script>mocha.setup('bdd');</script>
<script src="./ffmpeg.test.js"></script>
<script>
window.expect = chai.expect;
mocha.run();
</script>
</body>
</html>

View File

@@ -0,0 +1,69 @@
const { createFFmpeg } = FFmpeg;
describe('FS()', () => {
const ffmpeg = createFFmpeg(OPTIONS);
before(async function cb() {
this.timeout(0);
await ffmpeg.load();
});
it('should throw error when readdir for invalid path ', () => {
expect(() => ffmpeg.FS('readdir', '/invalid')).to.throw(/readdir/);
});
it('should throw error when readFile for invalid path ', () => {
expect(() => ffmpeg.FS('readFile', '/invalid')).to.throw(/readFile/);
});
it('should throw an default error ', () => {
expect(() => ffmpeg.FS('unlink', '/invalid')).to.throw(/Oops/);
});
});
describe('load()', () => {
it('should throw error when corePath is not a string', async () => {
const ffmpeg = createFFmpeg({ ...OPTIONS, corePath: null });
try {
await ffmpeg.load();
} catch (e) {
expect(e).to.be.an('Error');
}
});
it('should throw error when not called before FS() and run()', () => {
const ffmpeg = createFFmpeg(OPTIONS);
expect(() => ffmpeg.FS('readdir', 'dummy')).to.throw();
expect(() => ffmpeg.run('-h')).to.throw();
});
it('should throw error when running load() more than once', async () => {
const ffmpeg = createFFmpeg(OPTIONS);
await ffmpeg.load();
try {
await ffmpeg.load();
} catch (e) {
expect(e).to.be.an('Error');
}
}).timeout(TIMEOUT);
});
describe('isLoaded()', () => {
it('should return true when loaded', async () => {
const ffmpeg = createFFmpeg(OPTIONS);
await ffmpeg.load();
expect(ffmpeg.isLoaded()).to.equal(true);
}).timeout(TIMEOUT);
});
describe('run()', () => {
it('should not allow to run two command at the same time', async () => {
const ffmpeg = createFFmpeg(OPTIONS);
await ffmpeg.load();
ffmpeg.run('-h');
setTimeout(() => {
try {
ffmpeg.run('-h');
} catch (e) {
expect(e).to.be.an(Error);
}
}, 500);
}).timeout(TIMEOUT);
});