Compare commits
4
Commits
v0.1
...
encode-to-video
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fe343b0bb | ||
|
|
84ca8ec083 | ||
|
|
fc06741192 | ||
|
|
1401af6e81 |
@@ -0,0 +1,148 @@
|
||||
import beamcoder from "beamcoder";
|
||||
import EventEmitter from "node:events";
|
||||
import fs from "node:fs";
|
||||
import Stream from "node:stream";
|
||||
|
||||
const sleep = (waitTimeInMs) => new Promise((resolve) => setTimeout(resolve, waitTimeInMs));
|
||||
let c = 10;
|
||||
const readableStream = new Stream.Readable({
|
||||
async read() {
|
||||
await sleep(10);
|
||||
if (c < 810) {
|
||||
const data = fs.readFileSync(
|
||||
`captures/${parseInt(c / 10)
|
||||
.toString()
|
||||
.padStart(4, "0")}.jpg`,
|
||||
);
|
||||
this.push(data);
|
||||
} else {
|
||||
this.push(null);
|
||||
}
|
||||
c++;
|
||||
},
|
||||
});
|
||||
|
||||
const ee = new EventEmitter();
|
||||
|
||||
let demuxers = beamcoder.demuxers();
|
||||
// console.log(demuxers);
|
||||
|
||||
ee.on("inputFrame", (f) => {});
|
||||
|
||||
const makeEncoder = (frame, frameRate) => {
|
||||
return beamcoder.encoder({
|
||||
name: "libx264",
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
bit_rate: 400000,
|
||||
// qmin: 22,
|
||||
time_base: [1, frameRate],
|
||||
framerate: [frameRate, 1],
|
||||
pix_fmt: "yuv420p",
|
||||
preset: "faster",
|
||||
gop_size: 10,
|
||||
max_b_frames: 1,
|
||||
});
|
||||
};
|
||||
async function imageToVideo(imagePath, duration, frameRate = 20) {
|
||||
const muxTimeBase = 90000;
|
||||
let demuxerStream = beamcoder.demuxerStream({ highwaterMark: 65536 });
|
||||
|
||||
console.log("piping");
|
||||
readableStream.pipe(demuxerStream);
|
||||
console.log("creating demuxer");
|
||||
// Create a demuxer for the JPEG image
|
||||
// let demuxer = await beamcoder.demuxer(imagePath);
|
||||
let demuxer = await demuxerStream.demuxer({ name: "jpeg_pipe" });
|
||||
// Create a decoder for the image
|
||||
let decoder = beamcoder.decoder({ demuxer: demuxer, name: "mjpeg" });
|
||||
|
||||
// Read the image packet
|
||||
console.log("wait demuxer");
|
||||
// let packet = await demuxer.read();
|
||||
let encoder = null;
|
||||
let muxer = null;
|
||||
let vstr = null;
|
||||
let i = 0;
|
||||
while (true) {
|
||||
let packet = await demuxer.read();
|
||||
if (packet == null) break;
|
||||
let frames = await decoder.decode(packet);
|
||||
let frame = frames.frames[0];
|
||||
if (encoder == null) {
|
||||
encoder = makeEncoder(frame, frameRate);
|
||||
// Create an H.264 encoder
|
||||
// https://stackoverflow.com/a/13646293/3530257
|
||||
// > the codec unit of measurement is commonly set to the interval between
|
||||
// each frame and the next, > so that frame times are successive integers.
|
||||
|
||||
// TODO a muxer per client?
|
||||
let stream = beamcoder.muxerStream({});
|
||||
stream.pipe(fs.createWriteStream("test.mp4"));
|
||||
// Create a muxer for the output video
|
||||
muxer = stream.muxer({ format_name: "mp4" });
|
||||
|
||||
// console.log(demuxer.streams[0].codecpar.extradata); // null
|
||||
vstr = muxer.newStream({
|
||||
name: "h264",
|
||||
time_base: [1, muxTimeBase], // frameRate],
|
||||
interleaved: true,
|
||||
});
|
||||
|
||||
// the Object.assign is structural (!!)
|
||||
Object.assign(vstr.codecpar, {
|
||||
width: encoder.width,
|
||||
height: encoder.height,
|
||||
format: encoder.pix_fmt,
|
||||
});
|
||||
|
||||
await muxer.openIO();
|
||||
// adding "empty_moov" crashes mpv/ffmpeg
|
||||
// await muxer.initOutput({ movflags:
|
||||
// "frag_keyframe+default_base_moof+faststart" });
|
||||
await muxer.initOutput({ movflags: "frag_keyframe" });
|
||||
console.log("inited");
|
||||
// Add a video stream to the muxer
|
||||
await muxer.writeHeader();
|
||||
console.log("header written");
|
||||
}
|
||||
if (frame) {
|
||||
frame.pts = i; // << the successive integers
|
||||
frame.dts = i; // << the successive integers
|
||||
let encodedPackets = await encoder.encode(frame);
|
||||
|
||||
// Write the encoded packets to the output file
|
||||
for (let packet of encodedPackets.packets) {
|
||||
packet.duration = 1;
|
||||
packet.stream_index = vstr.index;
|
||||
packet.pts = (packet.pts * muxTimeBase) / frameRate;
|
||||
packet.dts = (packet.dts * muxTimeBase) / frameRate;
|
||||
// packet.pts = i;
|
||||
await muxer.writeFrame(packet);
|
||||
// outFile.write(packet.data);
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
// Finalize the encoder and muxer
|
||||
let encodedPackets = await encoder.flush();
|
||||
// after flushing the encoder, we may hve some more packets
|
||||
|
||||
// Write the encoded packets to the output file
|
||||
for (let packet of encodedPackets.packets) {
|
||||
packet.duration = 1;
|
||||
packet.stream_index = vstr.index;
|
||||
packet.pts = (packet.pts * muxTimeBase) / frameRate;
|
||||
packet.dts = (packet.dts * muxTimeBase) / frameRate;
|
||||
await muxer.writeFrame(packet);
|
||||
}
|
||||
await muxer.writeTrailer();
|
||||
}
|
||||
|
||||
// Usage example
|
||||
imageToVideo("captures/0010.jpg", 20)
|
||||
.then(() => {
|
||||
console.log("Video created successfully");
|
||||
})
|
||||
.catch(console.error);
|
||||
Generated
+41
@@ -4,6 +4,9 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"beamcoder": "^0.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^10.2.0",
|
||||
"ts-node": "^10.9.2",
|
||||
@@ -166,6 +169,16 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/beamcoder": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/beamcoder/-/beamcoder-0.7.1.tgz",
|
||||
"integrity": "sha512-20gpjy6V+9+X3v1H5wfofzc+MbVCPXv3JE4cm9y5R28pW2LRz2LSlJptiUxZJI8ips4xBoq7oYh+1LE5HvZskQ==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"segfault-handler": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
@@ -175,6 +188,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
@@ -381,6 +402,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -714,6 +740,11 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.18.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
|
||||
"integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w=="
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
|
||||
@@ -854,6 +885,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/segfault-handler": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/segfault-handler/-/segfault-handler-1.3.0.tgz",
|
||||
"integrity": "sha512-p7kVHo+4uoYkr0jmIiTBthwV5L2qmWtben/KDunDZ834mbos+tY+iO0//HpAJpOFSQZZ+wxKWuRo4DxV02B7Lg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.2.1",
|
||||
"nan": "^2.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
||||
|
||||
@@ -7,5 +7,8 @@
|
||||
"mocha": "^10.2.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"beamcoder": "^0.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user