Adopt lerna and typescript
BIN
packages/ffmpeg/examples/assets/StarWars3.wav
Normal file
BIN
packages/ffmpeg/examples/assets/flame.avi
Normal file
BIN
packages/ffmpeg/examples/assets/triangle/audio.ogg
Normal file
BIN
packages/ffmpeg/examples/assets/triangle/tmp.000.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.001.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.002.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.003.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.004.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.005.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.006.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.007.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.008.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.009.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.010.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.011.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.012.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.013.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.014.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.015.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.016.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.017.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.018.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.019.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.020.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.021.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.022.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.023.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.024.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.025.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.026.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.027.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.028.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.029.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.030.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.031.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.032.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.033.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.034.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.035.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.036.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.037.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.038.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.039.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.040.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.041.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.042.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.043.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.044.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.045.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.046.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.047.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.048.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.049.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.050.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.051.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.052.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.053.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.054.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.055.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.056.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.057.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.058.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
packages/ffmpeg/examples/assets/triangle/tmp.059.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
22
packages/ffmpeg/examples/browser/README.md
Normal 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
|
||||
55
packages/ffmpeg/examples/browser/concatDemuxer.html
Normal 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>
|
||||
53
packages/ffmpeg/examples/browser/image2video.html
Normal 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>
|
||||
57
packages/ffmpeg/examples/browser/transcode.html
Normal 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>
|
||||
48
packages/ffmpeg/examples/browser/transcode.worker.html
Normal 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>
|
||||
24
packages/ffmpeg/examples/browser/transcode.worker.js
Normal 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});
|
||||
}
|
||||
}
|
||||
|
||||
44
packages/ffmpeg/examples/browser/trim.html
Normal 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>
|
||||
72
packages/ffmpeg/examples/browser/webcam.html
Normal 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>
|
||||
13
packages/ffmpeg/examples/node/concatDemuxer.js
Executable 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);
|
||||
})();
|
||||
12
packages/ffmpeg/examples/node/hstack.js
Executable 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);
|
||||
})();
|
||||
22
packages/ffmpeg/examples/node/image2video.js
Executable 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);
|
||||
})();
|
||||
14
packages/ffmpeg/examples/node/multiple-output.js
Executable 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);
|
||||
})();
|
||||
14
packages/ffmpeg/examples/node/transcode.js
Executable 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);
|
||||
})();
|
||||
12
packages/ffmpeg/examples/node/trim.js
Executable 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);
|
||||
})();
|
||||
60
packages/ffmpeg/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@ffmpeg/ffmpeg",
|
||||
"version": "0.11.5",
|
||||
"description": "FFmpeg WebAssembly version",
|
||||
"main": "src/index.js",
|
||||
"types": "src/index.d.ts",
|
||||
"directories": {
|
||||
"example": "examples"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node scripts/server.js",
|
||||
"start:worker": "node scripts/worker-server.js",
|
||||
"build": "rimraf dist && webpack --config scripts/webpack.config.prod.js",
|
||||
"build:worker": "rimraf dist && webpack --config scripts/webpack.config.worker.prod.js",
|
||||
"prepublishOnly": "npm run build",
|
||||
"wait": "rimraf dist && wait-on http://localhost:3000/dist/ffmpeg.dev.js",
|
||||
"test": "npm-run-all -p -r start test:all",
|
||||
"test:all": "npm-run-all wait test:browser:ffmpeg test:node:all",
|
||||
"test:node": "node node_modules/mocha/bin/_mocha --exit --bail --require ./scripts/test-helper.js",
|
||||
"test:node:all": "npm run test:node -- ./tests/*.test.js",
|
||||
"test:browser": "mocha-headless-chrome -a allow-file-access-from-files -a incognito -a no-sandbox -a disable-setuid-sandbox -a disable-logging -t 300000",
|
||||
"test:browser:ffmpeg": "npm run test:browser -- -f ./tests/ffmpeg.test.html"
|
||||
},
|
||||
"browser": {
|
||||
"./src/node/index.js": "./src/browser/index.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ffmpegwasm/ffmpeg.wasm.git"
|
||||
},
|
||||
"keywords": [
|
||||
"ffmpeg",
|
||||
"WebAssembly",
|
||||
"video"
|
||||
],
|
||||
"author": "Jerome Wu <jeromewus@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ffmpegwasm/ffmpeg.wasm/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.16.1"
|
||||
},
|
||||
"homepage": "https://github.com/ffmpegwasm/ffmpeg.wasm#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-url": "^1.2.4",
|
||||
"node-fetch": "^2.6.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"resolve-url": "^0.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.37.0",
|
||||
"@typescript-eslint/parser": "^5.37.0",
|
||||
"eslint": "^8.23.1",
|
||||
"typescript": "^4.8.3"
|
||||
}
|
||||
}
|
||||
10
packages/ffmpeg/src/browser/defaultOptions.js
Normal 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 };
|
||||
38
packages/ffmpeg/src/browser/fetchFile.js
Normal 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);
|
||||
};
|
||||
114
packages/ffmpeg/src/browser/getCreateFFmpegCore.js
Normal 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,
|
||||
});
|
||||
};
|
||||
5
packages/ffmpeg/src/browser/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import defaultOptions from './defaultOptions';
|
||||
import { getCreateFFmpegCore } from './getCreateFFmpegCore';
|
||||
import { fetchFile } from './fetchFile';
|
||||
|
||||
export { defaultOptions, getCreateFFmpegCore, fetchFile };
|
||||
50
packages/ffmpeg/src/config.js
Normal 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: '',
|
||||
},
|
||||
};
|
||||
279
packages/ffmpeg/src/createFFmpeg.js
Normal 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/ffmpeg/src/index.d.ts
vendored
Normal 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>;
|
||||
36
packages/ffmpeg/src/index.js
Normal 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,
|
||||
};
|
||||
6
packages/ffmpeg/src/node/defaultOptions.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
* Default options for node environment
|
||||
*/
|
||||
module.exports = {
|
||||
corePath: '@ffmpeg/core',
|
||||
};
|
||||
33
packages/ffmpeg/src/node/fetchFile.js
Normal 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);
|
||||
};
|
||||
7
packages/ffmpeg/src/node/getCreateFFmpegCore.js
Normal 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) });
|
||||
});
|
||||
9
packages/ffmpeg/src/node/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const defaultOptions = require('./defaultOptions');
|
||||
const getCreateFFmpegCore = require('./getCreateFFmpegCore');
|
||||
const fetchFile = require('./fetchFile');
|
||||
|
||||
module.exports = {
|
||||
defaultOptions,
|
||||
getCreateFFmpegCore,
|
||||
fetchFile,
|
||||
};
|
||||
11
packages/ffmpeg/src/utils/errors.js
Normal 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,
|
||||
};
|
||||
24
packages/ffmpeg/src/utils/log.js
Normal 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,
|
||||
};
|
||||
10
packages/ffmpeg/src/utils/parseArgs.js
Normal 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];
|
||||
};
|
||||
6
packages/ffmpeg/tests/.eslintrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-undef": 0,
|
||||
"camelcase": 0
|
||||
}
|
||||
}
|
||||
19
packages/ffmpeg/tests/constants.js
Normal 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,
|
||||
};
|
||||
}
|
||||
21
packages/ffmpeg/tests/ffmpeg.test.html
Normal 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>
|
||||
69
packages/ffmpeg/tests/ffmpeg.test.js
Normal 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);
|
||||
});
|
||||
3
packages/ffmpeg/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||